kopscript 0.2.0 → 0.4.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/LLM.md ADDED
@@ -0,0 +1,457 @@
1
+ # KopScript — LLM reference
2
+
3
+ Complete, dense reference for generating correct KopScript (`.ks`) code. This is a spec,
4
+ not a tutorial — see `README.md` for narrative explanation and rationale. Every construct
5
+ below is exhaustive: if something isn't listed here, it doesn't exist in the language.
6
+ Read the "Does not exist" section before assuming any feature from another language
7
+ carries over.
8
+
9
+ Compiles to plain ES modules.
10
+
11
+ ## CLI
12
+
13
+ ```
14
+ ks build <file.ks> # type-check + emit <file>.js next to the source
15
+ ks run <file.ks> # build, then execute with node
16
+ ks watch <file.ks> # build, then rebuild on every change to any file in the graph
17
+ ks check <file.ks> # type-check only, no output files
18
+ ks build|check <file.ks> --json # single JSON object on stdout instead of human text
19
+ ```
20
+
21
+ `--json` output shape (also what a tool/agent should parse instead of scraping text):
22
+
23
+ ```json
24
+ { "success": false, "diagnostics": [{ "severity": "error", "message": "...", "line": 2, "col": 14, "file": "/abs/path.ks" }], "written": [] }
25
+ ```
26
+
27
+ `written` is the absolute paths actually written (`build` only, and only on success — a
28
+ failed `build` writes nothing). Exit code is `0` iff `success` is `true`. A missing entry
29
+ file yields `{ success: false, diagnostics: [], written: [], error: "cannot find file '...'" }`.
30
+
31
+ ## File shape
32
+
33
+ ```ks
34
+ using "./other_file"; // 0+ usings, must be a contiguous prefix at the top
35
+ using "../lib/thing"; // relative only ("./" or "../"), no bare package names
36
+
37
+ // then any number of top-level statements/declarations, in any order
38
+ ```
39
+
40
+ `using` resolves `<path>.ks` relative to the current file and pulls its `public`
41
+ top-level classes/interfaces/enums/functions/extern-decls into unqualified scope. No
42
+ package-import mechanism — cross-package consumption goes through `extern` instead (see
43
+ below). No circular `using` (compile error).
44
+
45
+ ## Types
46
+
47
+ | Type | Notes |
48
+ |---|---|
49
+ | `number` | one numeric type, no int/float split |
50
+ | `string` | |
51
+ | `bool` | `true` / `false` |
52
+ | `void` | function/method return type only |
53
+ | `T[]` | array of `T`; `T[][]` etc. nest freely |
54
+ | `ClassName` / `InterfaceName` / `EnumName` | any user-declared named type |
55
+ | `(T1, T2) => R` | function type — parenthesized param types, arrow, return type |
56
+ | `task` / `task<T>` | see Async below |
57
+ | `state<T>` | see Reactive state below |
58
+ | `T?` | nullable — see Nullable types below |
59
+ | `Box<T>` | a user-declared generic class/interface — see Generics below |
60
+
61
+ **Does not exist**: `any`/`unknown` as a writable annotation, a `let`/`var` keyword (see
62
+ Declarations below — there isn't one), type inference for declarations (every
63
+ local/`const`/param/field/return type is written out explicitly), union types, tuples,
64
+ structural/duck typing (all typing is nominal). Generics exist but are deliberately
65
+ scoped down — see the Generics section for exactly what's NOT supported there (multiple
66
+ type parameters, constraints, generic functions, generic inheritance).
67
+
68
+ ## Declarations
69
+
70
+ ```ks
71
+ number x = 5; // type-first local var (mutable)
72
+ const string name = "Joe"; // type-first const
73
+ ```
74
+
75
+ Top-level declarations (function/class/interface/enum/extern/raw-string) default to
76
+ **exported** (visible via `using`) unless marked `private`. A top-level bare `number x = 5;`
77
+ is a plain statement, not exportable — `using` only pulls in types and functions, never
78
+ values.
79
+
80
+ ```ks
81
+ private void Helper() { } // file-local, not visible via `using`
82
+ void Exported() { } // default: visible via `using`
83
+ ```
84
+
85
+ ### Functions
86
+
87
+ ```ks
88
+ number Add(number a, number b) {
89
+ return a + b;
90
+ }
91
+ async task<number> FetchValue() { return 1; } // see Async
92
+ ```
93
+
94
+ ### Classes
95
+
96
+ ```ks
97
+ class Dog : Animal, IBark { // 0-1 base class + 0+ interfaces, comma-separated
98
+ private string name; // field: no initializer syntax — set in constructor
99
+ public static number InstanceCount = 0; // static field: initializer REQUIRED (no ctor runs for it)
100
+
101
+ public string Name { get; set; } // auto-property, get-only or get+set
102
+ public string ReadOnly { get; } // settable only within this class's own constructor
103
+
104
+ constructor(string name) : base(/* base ctor args */) {
105
+ this.name = name;
106
+ }
107
+
108
+ public virtual string Speak() { return "..."; } // overridable
109
+ public override string Speak() { return "Woof"; } // in a subclass
110
+ public static Dog Create(string name) { return new Dog(name); }
111
+ }
112
+
113
+ Dog d = new Dog("Rex");
114
+ ```
115
+
116
+ - Visibility on members: `public` / `private` / `protected` (default when omitted: `public`).
117
+ - `virtual`/`override` only on instance methods, not static, not fields/properties.
118
+ - No interface-implementation keyword — implementing is structural-by-declaration: list
119
+ the interface in the base list and provide matching public methods, checked at compile time.
120
+ - No nested classes/functions.
121
+ - No multiple inheritance (at most one class in the base list; the rest must be interfaces).
122
+ - `class Box<T> { ... }` — a single type parameter is allowed; see Generics below for the
123
+ full (deliberately scoped-down) rules.
124
+
125
+ ### Interfaces
126
+
127
+ ```ks
128
+ interface IShape {
129
+ number Area(); // method signatures only — no fields/properties in v1
130
+ }
131
+ interface INamedShape : IShape { // interfaces can extend other interfaces (not classes)
132
+ string GetName();
133
+ }
134
+ interface IContainer<T> { // interfaces take a single type parameter too — see Generics
135
+ T Get();
136
+ }
137
+ ```
138
+
139
+ ### Enums
140
+
141
+ ```ks
142
+ enum Color { Red, Green, Blue }
143
+ Color c = Color.Red;
144
+ ```
145
+
146
+ No custom values, no methods on enums.
147
+
148
+ ### `match` expression
149
+
150
+ ```ks
151
+ string Classify(string input) {
152
+ return match input {
153
+ "cat", "dog" => "animal", // comma = OR within one arm
154
+ r"^[0-9]+$" => "number", // regex pattern, r"..."
155
+ _ => "unknown" // wildcard REQUIRED — exhaustiveness-checked
156
+ };
157
+ }
158
+ ```
159
+
160
+ `match` is an expression (has a value), matches on literal/regex patterns only (not
161
+ type patterns, not destructuring), and always requires a trailing `_` arm.
162
+
163
+ ## Statements
164
+
165
+ ```ks
166
+ if (cond) { } else if (cond2) { } else { }
167
+ while (cond) { }
168
+ for (number i = 0; i < n; i = i + 1) { }
169
+ foreach (number x in items) { } // arrays only
170
+ break; continue; return; return expr;
171
+ try { } catch (string e) { } finally { } // at most one catch clause; param type is your choice
172
+ throw "message"; // throw takes any expression, not just string
173
+ ```
174
+
175
+ `throw` accepts any expression (no base exception/error type to conform to), and a
176
+ `catch (T e)` param's type `T` is **not checked against what's actually thrown** — it
177
+ compiles to a plain JS `catch (e) { ... }` with zero runtime type discrimination. The
178
+ type annotation only affects what `e` type-checks as *inside* the catch block. There's
179
+ no way to have multiple catch clauses per type (only one `catch` per `try`), and no
180
+ built-in exception hierarchy — `catch (string e)` (matching `throw "message";`) is the
181
+ conventional shape used throughout this codebase, but any type is legal.
182
+
183
+ There is **no ternary/conditional expression** (`if` is a statement only). To get a
184
+ conditional *value* inline, use a `match` on a `bool`-shaped case set, or (in Kopular UI
185
+ code) the `If()` helper — see the Kopular reference.
186
+
187
+ ## Expressions
188
+
189
+ Precedence, low → high: `=` (right-assoc) → `||` → `&&` → `==` `!=` → `<` `>` `<=` `>=`
190
+ → `+` `-` → `*` `/` `%` → unary `-` `!` → postfix (`.member`, `(call)`, `[index]`) → primary.
191
+
192
+ ```ks
193
+ this
194
+ new ClassName(args)
195
+ obj.Member
196
+ obj.Method(args)
197
+ arr[index]
198
+ await someTask // only inside an async function/method body
199
+ state(0) // constructs a state<T> — see below
200
+ $"Hello {name}, you are {age} years old" // interpolated string
201
+ r"^[a-z]+$" // regex literal (only meaningful as a match pattern)
202
+ ```
203
+
204
+ ### Lambdas
205
+
206
+ ```ks
207
+ (number x) => x * x // expression body
208
+ (number x, number y) => { return x + y; } // block body
209
+ () => this.DoThing()
210
+ ```
211
+
212
+ **Every lambda parameter type is required — no inference, no bare `(x) => ...`.** A
213
+ lambda's own first parameter cannot itself be a function type (a rare v1 parser
214
+ limitation, not a semantic one).
215
+
216
+ ### Arrays
217
+
218
+ ```ks
219
+ number[] xs = [1, 2, 3];
220
+ xs.Length
221
+ xs.Push(4) // NON-mutating — returns a new array, doesn't modify xs
222
+ xs.Map((number x) => x * 2) // -> array of whatever the callback returns
223
+ xs.Filter((number x) => x > 1)
224
+ xs.ForEach((number x) => { print(x); })
225
+ ```
226
+
227
+ ### Strings
228
+
229
+ ```ks
230
+ s.Length
231
+ s.Contains(other) / s.StartsWith(other) / s.EndsWith(other)
232
+ s.Replace(from, to)
233
+ s.Split(sep) // -> string[]
234
+ s.Trim() / s.ToUpper() / s.ToLower()
235
+ "a" + "b" // + is also string concatenation
236
+ ```
237
+
238
+ ### Async
239
+
240
+ ```ks
241
+ async task<number> FetchValue() {
242
+ number x = await SomeOtherAsyncFn();
243
+ return x;
244
+ }
245
+ async task DoThing() { } // bare task = "async void" with a value you can await
246
+ ```
247
+
248
+ `async` requires (and is required by) a `task`/`task<T>` return type. `await` only
249
+ legal inside an async function/method body. No async lambdas. No way to construct a
250
+ `task` value from outside an async function body.
251
+
252
+ ### Reactive state — `state<T>`
253
+
254
+ ```ks
255
+ state<number> count = state(0);
256
+ count.Value // read/write the current value
257
+ count.Value = count.Value + 1;
258
+ count.Subscribe((number v) => { ... }); // called on every future .Value assignment
259
+ ```
260
+
261
+ Not a use of user-declared generics (see below) — `state<T>` is one of exactly two
262
+ hardcoded parametrized forms in the type system (the other is `task<T>`), each with only
263
+ ever one built-in meaning, unlike a real generic class/interface.
264
+
265
+ ## Generics — a single type parameter on classes/interfaces
266
+
267
+ ```ks
268
+ class Box<T> {
269
+ public T Value;
270
+ constructor(T v) { this.Value = v; }
271
+ public T Get() { return this.Value; }
272
+ }
273
+
274
+ Box<number> nb = new Box<number>(5); // type argument required on both the type AND `new`
275
+ Box<string> sb = new Box<string>("hi");
276
+ print(nb.Get()); // 5
277
+ ```
278
+
279
+ One unconstrained, invariant type parameter per class/interface — the whole feature.
280
+ Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
281
+ compile to the exact same plain `class Box`), so there's no runtime cost and no way to
282
+ inspect `T` at runtime.
283
+
284
+ **Rules:**
285
+ - The type argument is **required everywhere** a generic type is named — on the variable
286
+ type (`Box<number>`) and separately on `new` (`new Box<number>(...)`); a bare `Box` (no
287
+ argument) is a compile error, not "any"/inferred.
288
+ - **Invariant**: `Box<Dog>` is not assignable to `Box<Animal>` even if `Dog : Animal` —
289
+ type arguments must match exactly, not just be compatible.
290
+ - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`.
291
+ - Inside the class/interface's own body, `T` is fully abstract — you can store it, return
292
+ it, pass it around, but (unconstrained) you **cannot call any member on a bare `T`
293
+ value** (`this.value.Foo()` is a compile error: "Cannot access member 'Foo' on type
294
+ 'T'"). This is correct, not a bug — same as an unconstrained type parameter in
295
+ C#/Java/TypeScript.
296
+
297
+ **Does not exist (v1 scope cuts, each deliberate)**:
298
+ - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
299
+ take a type parameter, not free functions/methods themselves (a method *inside* a
300
+ generic class can use that class's own `T` freely, same as any other member).
301
+ - **Multiple type parameters.** No `Map<K, V>` — exactly one `<T>` or none.
302
+ - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
303
+ which is why you can't call members on a bare `T` (see above).
304
+ - **Generic inheritance.** A class/interface's base list can only name a *non-generic*
305
+ type — `class Foo : Box<number>` and even `class Foo<T> : SomeGenericBase<T>` are both
306
+ compile errors ("cannot extend/implement generic type '...' — not supported in v1"). A
307
+ generic class/interface can still extend/implement ordinary non-generic bases normally.
308
+ - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
309
+
310
+ ## Nullable types — `T?`
311
+
312
+ ```ks
313
+ string? maybeName = null; // null literal, any type-position postfix `?`
314
+ string?[] arrOfNullable = [null, "x"]; // array OF nullable strings
315
+ string[]? nullableArr = null; // NULLABLE array of strings — order matters
316
+ ```
317
+
318
+ `?` is a postfix modifier on any type, erased at codegen (JS already has native `null` —
319
+ this is purely a compile-time distinction). Assignability is one-directional: a plain `T`
320
+ is always usable where `T?` is expected (widening), but a `T?` is **never** usable where
321
+ `T` is expected without narrowing first — accessing a member or index on an un-narrowed
322
+ `T?` is a compile error:
323
+
324
+ ```ks
325
+ void F(string? s) {
326
+ print(s.Length); // ERROR: possibly-null type 'string?'
327
+ if (s != null) {
328
+ print(s.Length); // OK — s is string here, not string?
329
+ }
330
+ print(s.Length); // ERROR again — narrowing doesn't survive past the if
331
+ }
332
+ ```
333
+
334
+ **Narrowing is recognized in exactly these shapes** (anything else doesn't narrow):
335
+ - `if (x != null) { ... }` — narrows `x` inside the then-branch.
336
+ - `if (x == null) { ... } else { ... }` — narrows `x` inside the else-branch.
337
+ - `cond1 && x.Foo` where `cond1` is `x != null` — narrows `x` for the right operand only.
338
+ - `cond1 || x.Foo` where `cond1` is `x == null` — narrows `x` for the right operand only
339
+ (De Morgan's: reaching the right side of `||` means the left was false).
340
+ - Either operand order works (`x != null` and `null != x` both narrow `x`).
341
+
342
+ **Narrowing does NOT handle** (a common instinct from other languages — don't rely on
343
+ these): an early-return guard clause (`if (x == null) { return; } use(x);` — `x` is
344
+ *not* narrowed after the `if`, since that needs control-flow reachability analysis, not
345
+ just scope); narrowing a member/property path (`if (this.Field != null)` narrows
346
+ nothing — only a bare local/parameter identifier narrows); narrowing surviving
347
+ reassignment (assigning a new value to a narrowed variable inside the narrowed block
348
+ doesn't invalidate or restore narrowing — avoid reassigning a narrowed variable within
349
+ the block). `x` stays narrowed for the rest of a narrowed block, including nested blocks
350
+ inside it, since narrowing is scope-based.
351
+
352
+ Comparing a value to `null` when its type **can't** be null is a compile error, not a
353
+ silently-always-false comparison — this is a real bug-catching check, not just a style
354
+ rule:
355
+
356
+ ```ks
357
+ class Dog { }
358
+ Dog d = new Dog();
359
+ bool x = d == null; // ERROR: Type 'Dog' can never be null
360
+ ```
361
+
362
+ ## `extern` — describing existing JS/npm code
363
+
364
+ Three forms, always terminated with `;` after the closing brace for the class form:
365
+
366
+ ```ks
367
+ extern number ParseFloat(string s) from "npm-package" as "parseFloat"; // `from` before `as`; omit `from` for an ambient global
368
+ extern SomeType globalThing as "actualJsName"; // extern value, ambient
369
+ extern class Widget {
370
+ constructor();
371
+ string label { get; set; } // property sig — get / get+set
372
+ static number Count { get; }
373
+ void Render();
374
+ virtual void Update(); // lets a real subclass `override` it
375
+ } from "npm-package";
376
+ ```
377
+
378
+ Clause order is always `from "<path>"` then `as "<jsName>"` (both optional, `from`
379
+ first if both are present). `from "..."` omitted = ambient global (assumed to already
380
+ exist at runtime, e.g. `document`). `as "..."` omitted = the JS-side name matches the
381
+ KopScript-declared name exactly. Extern class members use real JS member names verbatim
382
+ (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
383
+ declarations — each stands alone.
384
+
385
+ **Never write `async` on an extern function/method signature** — declare its return type
386
+ as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
387
+ `async` only means something for a *body* the checker validates (legalizing `await`
388
+ inside it, checking `return` against the unwrapped type); an extern signature has no
389
+ body, so the keyword is simply invalid there — a parse error, not a no-op. This applies
390
+ equally to a top-level `extern` function and an `extern class` method signature.
391
+
392
+ `jsName` (the `as "..."` string) can be a dotted path, not just a bare identifier — e.g.
393
+ `as "JSON.parse"` — since it's spliced directly into `globalThis.<jsName>` for an
394
+ ambient binding. This is the supported way to get a *typed* (unchecked, trust-based)
395
+ JSON value: describe the shape as its own `extern class`, then declare a parse function
396
+ for it: `extern MyShape ParseIt(string json) as "JSON.parse";` — calling `ParseIt(text)`
397
+ returns whatever `JSON.parse` actually parsed, typed as `MyShape` with zero runtime
398
+ verification (the same trust model as every other `extern`).
399
+
400
+ An `extern class` reachable from a real npm package — not same-project `using`, which
401
+ only resolves relative `.ks` paths — is how KopScript consumes *any* JS dependency,
402
+ including another KopScript-authored package published as JS (e.g. Kopular): describe
403
+ exactly the members used, from that package's real module specifier. See Kopular's own
404
+ `LLM.md`/README for a live example (`Component`, `Router`, `Http`, ...).
405
+
406
+ ## `raw string` — compile-time file embedding
407
+
408
+ ```ks
409
+ raw string HeroHtml from "./hero.html"; // reads the file at compile time, type is always string
410
+ ```
411
+
412
+ ## Keywords (reserved, lowercase, exact match)
413
+
414
+ `using extern raw from as const class interface enum constructor public private
415
+ protected static virtual override get set return if else while for foreach in break
416
+ continue match this base new void true false null task state async await try catch
417
+ finally throw`
418
+
419
+ Anything else (including PascalCase versions of the above, e.g. `If`, `Match`) is a
420
+ valid identifier.
421
+
422
+ ## Naming convention used throughout the ecosystem
423
+
424
+ Types, methods, and properties: **PascalCase** (`Speak()`, `Render()`, `Name`).
425
+ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
426
+
427
+ ## Does not exist (don't reach for these)
428
+
429
+ Generics · `any`/`unknown` annotations ·
430
+ a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
431
+ reflection · type inference on declarations · ternary expression · union/tuple types ·
432
+ **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
433
+ the grammar, not just as a type; a JS API whose signature needs one — `fetch`'s options
434
+ argument, `addEventListener`'s options object, etc. — can't be called directly from
435
+ KopScript at all; see Kopular's `http.ks`/`http_runtime.js` for the one established
436
+ workaround, a small hand-written JS shim, not a language feature) ·
437
+ structural typing · multiple class inheritance ·
438
+ nested classes/functions · method overloading · default/optional parameters · varargs ·
439
+ static properties with accessors (static fields only) · interface properties (interface
440
+ *methods* only) · a real exception hierarchy or multi-type `catch` (one `catch` per
441
+ `try`, its param type is unchecked against what's actually thrown) · a package-import
442
+ mechanism (`using` is same-project only; cross-package = `extern`).
443
+
444
+ **Sharp edge, not just "unsupported":** declaring two methods with the same name in one
445
+ class is **not a compile error** — the second declaration silently replaces the first in
446
+ the checker's method table, so call sites resolve against whichever was declared last
447
+ (and its body is the only one ever emitted). If you want to offer more than one
448
+ signature, use different names, not overloading. (Nested function/class/interface/enum
449
+ declarations, by contrast, *are* a clean compile error: "Nested ... declarations are not
450
+ supported".)
451
+
452
+ ## Diagnostics
453
+
454
+ Every compiler error/warning is `{ severity, message, line, col }` (1-based). CLI output
455
+ format: `` file:line:col - severity: message `` plus a source line and a `^` pointer.
456
+ `DiagnosticBag.hasErrors` gates whether codegen runs at all — a program with any error
457
+ produces no output.
package/README.md CHANGED
@@ -11,6 +11,10 @@ auto-properties, enums) rather than TypeScript ones.
11
11
  Because KopScript compiles to plain JavaScript and runs on Node, it runs identically on Windows,
12
12
  macOS, and Linux — there's no native toolchain to maintain.
13
13
 
14
+ Generating KopScript with an AI coding assistant? Point it at **[`LLM.md`](./LLM.md)** —
15
+ a dense, complete, example-driven language spec designed to be loaded straight into an
16
+ LLM's context, as opposed to this README's narrative explanation.
17
+
14
18
  ## Highlights
15
19
 
16
20
  - **Object-oriented, C#-flavored**: type-first declarations (`string Name;`, not `name: string`),
@@ -18,6 +22,13 @@ macOS, and Linux — there's no native toolchain to maintain.
18
22
  explicit `virtual`/`override` dispatch (methods are sealed unless marked `virtual`), and a full
19
23
  `public`/`protected`/`private` access model enforced at compile time.
20
24
  - **Strongly typed**: every declaration is explicitly typed and checked at compile time.
25
+ - **Generics**: a single, unconstrained, invariant type parameter on classes and
26
+ interfaces (`class Box<T> { public T Value; }`) — erased at codegen with zero runtime
27
+ cost, the same way `task<T>`/`state<T>` already are.
28
+ - **Nullable types with compiler-enforced null-checking**: `T?` (`string?`, `Dog?`,
29
+ `number[]?`) — a `T?` can't be used where a `T` is expected without an `if (x != null)`
30
+ check first (checked statically, not just at runtime), and comparing a value that can
31
+ never be null to `null` is itself a compile error.
21
32
  - **Built-in string pattern matching**: a `match` expression over strings supporting literal,
22
33
  wildcard, and regex patterns — no `if`/`else` chains required.
23
34
  - **Real multi-file programs**: `using "./shapes";` compiles a whole dependency graph
@@ -40,9 +51,9 @@ macOS, and Linux — there's no native toolchain to maintain.
40
51
  `.Subscribe((T) => void)`) for holding state and reacting to it changing — no
41
52
  Observables, no operators, no manual unsubscribe bookkeeping.
42
53
  - **A companion framework, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)**:
43
- components, constructor-injected services (no DI container), and hash-based routing (no
44
- config DSL) — built entirely on the features above, in a separate repo consumed as a
45
- real package. [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
54
+ components, constructor-injected services via a composition root (no DI container), and
55
+ real-URL routing (no config DSL) — built entirely on the features above, in a separate
56
+ repo consumed as a real package. [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
46
57
  is a real app built on both.
47
58
 
48
59
  ## Getting started
@@ -196,6 +207,49 @@ void Announce(ISpeaker s) {
196
207
  }
197
208
  ```
198
209
 
210
+ ### Generics
211
+
212
+ Classes and interfaces can take a single type parameter:
213
+
214
+ ```ks
215
+ class Box<T> {
216
+ public T Value;
217
+ constructor(T v) { this.Value = v; }
218
+ public T Get() { return this.Value; }
219
+ }
220
+
221
+ Box<number> nb = new Box<number>(5);
222
+ Box<string> sb = new Box<string>("hi");
223
+ print(nb.Get()); // 5
224
+ ```
225
+
226
+ The type argument is required everywhere the generic type is named — on the variable's
227
+ declared type *and* separately on `new` (`Box<number> nb = new Box<number>(5);`, not
228
+ just one or the other). Generics are erased at codegen exactly like `task<T>`/`state<T>`
229
+ already are — `Box<number>` and `Box<string>` compile to the identical plain `class Box`,
230
+ so there's no runtime cost.
231
+
232
+ This is deliberately the smallest useful slice of generics, not a scaled-down promise of
233
+ more later inside v1:
234
+
235
+ - **One type parameter, invariant, unconstrained.** No `Map<K, V>` (multiple parameters),
236
+ no `T : ISomething` (constraints) — and because `T` is fully unconstrained, you can't
237
+ call any member on a bare `T` value inside the generic class's own body (that's correct
238
+ behavior, the same restriction C#/Java/TypeScript put on an unconstrained parameter, not
239
+ a bug). Invariant means `Box<Dog>` is **not** assignable to `Box<Animal>` even though
240
+ `Dog : Animal` — type arguments must match exactly.
241
+ - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
242
+ interfaces take a type parameter. A method *inside* a generic class can still use that
243
+ class's own `T` freely; it's just not introducing a type parameter of its own.
244
+ - **No generic inheritance.** A class or interface's base list can only name a
245
+ *non-generic* type. `class Foo : Box<number>` and `class Foo<T> : SomeBase<T>` are both
246
+ compile errors — a generic class can still extend/implement ordinary non-generic bases
247
+ normally, it just can't be the one on either side of a generic base relationship.
248
+
249
+ See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
250
+ against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`) and generic
251
+ interfaces used as standalone parameter types both work and are covered there.
252
+
199
253
  ### Enums
200
254
 
201
255
  ```ks
@@ -513,8 +567,41 @@ foreach (number item in items) {
513
567
  ### Types (v1 scope)
514
568
 
515
569
  `number`, `string`, `bool`, `void`, `T[]` (arrays), class types, interface types, enum
516
- types, and function types (`(T, ...) => R`). No generics or nullable types yet a
517
- possible future extension.
570
+ types, function types (`(T, ...) => R`), nullable types (`T?`, with `null` and
571
+ compiler-enforced null-checking — see "Nullable types" below), and a single type
572
+ parameter on classes/interfaces (`Box<T>` — see "Generics" above).
573
+
574
+ ### Nullable types — `T?`
575
+
576
+ ```ks
577
+ string? maybeName = null;
578
+ void Greet(string? name) {
579
+ if (name != null) {
580
+ print("Hello, " + name); // name is `string` here, not `string?`
581
+ } else {
582
+ print("Hello, stranger");
583
+ }
584
+ }
585
+ ```
586
+
587
+ `?` is a postfix modifier on any type (`Dog?`, or an array either way — `string?[]` is an
588
+ array of nullable strings, `string[]?` is a nullable array of strings; order matters).
589
+ It's purely a compile-time distinction — erased at codegen, since JS already has native
590
+ `null`. The compiler enforces it in both directions:
591
+
592
+ - A `T?` can never be used where a `T` is expected — accessing a member or index on one
593
+ is a compile error — **unless** it's been narrowed by an `if (x != null)` /
594
+ `if (x == null) {...} else {...}` check (either operand order, and inside a `&&`/`||`
595
+ short-circuit) on that exact local/parameter. Narrowing doesn't survive past the
596
+ checked block, doesn't follow a `this.Field` path (locals/params only), and doesn't
597
+ understand an early-return guard clause (`if (x == null) { return; }`) — each of those
598
+ would need real control-flow analysis, deliberately not taken on in v1.
599
+ - Comparing a value to `null` when its type *can't* be null (i.e. isn't itself a `T?`) is
600
+ a compile error, not a silently-always-false comparison — a real static check, not
601
+ just a style rule.
602
+
603
+ See `LLM.md`'s "Nullable types" section for the exhaustive rules if you're generating
604
+ code against this.
518
605
 
519
606
  ### Built-ins
520
607
 
@@ -534,7 +621,7 @@ src/
534
621
  codegen.ts AST -> JavaScript source string (readable ES2020 output)
535
622
  modules.ts multi-file orchestration: resolves the `using` graph, checks
536
623
  modules in dependency order, drives codegen across all of them
537
- cli.ts `ks build <file>` / `ks run <file>` entry point
624
+ cli.ts `ks build|run|watch|check <file>` entry point
538
625
  examples/ sample .ks programs (examples/modules/ is a multi-file one)
539
626
  test/ vitest unit and end-to-end tests
540
627
  ```
@@ -561,11 +648,32 @@ runtime representation at all and are dropped from the emitted JS entirely.
561
648
  ks build <file.ks> # type-check and emit <file>.js next to the source
562
649
  ks run <file.ks> # build, then execute the emitted JS with node
563
650
  ks watch <file.ks> # build, then rebuild on every change to any file in the graph
651
+ ks check <file.ks> # type-check only — no output files written
564
652
  ```
565
653
 
566
- During development, use `npm run ks -- <build|run|watch> <file.ks>` (backed by `tsx`),
567
- or run `npm run build` to compile the TypeScript compiler itself to `dist/` and use `node
568
- dist/cli.js` directly.
654
+ `build` and `check` both accept `--json`, which replaces all human-readable output with a
655
+ single JSON object on stdout meant for CI or a tool/agent parsing the result
656
+ programmatically instead of scraping formatted text:
657
+
658
+ ```json
659
+ {
660
+ "success": false,
661
+ "diagnostics": [
662
+ { "severity": "error", "message": "Argument 2 has type 'string', expected 'number'", "line": 2, "col": 14, "file": "/abs/path/to/file.ks" }
663
+ ],
664
+ "written": []
665
+ }
666
+ ```
667
+
668
+ `written` lists the absolute paths of every `.js` file actually written (always empty for
669
+ `check`, and for `build` on failure — nothing is written unless the whole graph is
670
+ error-free). A missing entry file reports `{ "success": false, "diagnostics": [],
671
+ "written": [], "error": "cannot find file '...'" }` instead of throwing. Exit code is 0
672
+ exactly when `success` is `true`, both with and without `--json`.
673
+
674
+ During development, use `npm run ks -- <build|run|watch|check> <file.ks>` (backed by
675
+ `tsx`), or run `npm run build` to compile the TypeScript compiler itself to `dist/` and
676
+ use `node dist/cli.js` directly.
569
677
 
570
678
  `watch` rebuilds on a save to *any* `.ks` file it reached while compiling — the entry and
571
679
  everything it (transitively, non-transitively per-file) `using`s — not just the entry
@@ -588,25 +696,27 @@ into your extensions folder).
588
696
 
589
697
  ## Status
590
698
 
591
- This is a v1 / hobby-project scope. Not yet supported: generics, nullable types, static
592
- auto-properties, interface properties (methods only), and nested functions/classes.
593
- Generics and nullable types in particular are a substantially bigger undertaking than
594
- everything else here they touch the type system's core (type parameters, variance,
595
- constraint checking) rather than being additive features, so they're deliberately left
596
- for a dedicated future pass rather than bolted on.
699
+ This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (a single
700
+ unconstrained, invariant type parameter on classes/interfaces) have both shipped — see
701
+ above for both. What generics deliberately doesn't cover: multiple type parameters
702
+ (`Map<K, V>`), constraints (`T : IFoo`), generic functions, generic inheritance, and
703
+ variance each a real, separable extension rather than a v1 oversight. Also not yet
704
+ supported: static auto-properties, interface properties (methods only), and nested
705
+ functions/classes.
597
706
 
598
707
  `async`/`await`, `task<T>`, and `try`/`catch`/`finally`/`throw` are now in place (see the
599
708
  language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
600
709
  and no way to build a `task` value by hand outside an `async` function body.
601
710
 
602
- ### Toward a frontend framework: Kopular
711
+ ### A frontend framework: Kopular
603
712
 
604
713
  The language shape — modules, closures, DOM interop, `state<T>`, and `extern`/`virtual`
605
714
  subclassing across a package boundary — exists to support real UI components, not just
606
715
  scripts. That framework itself, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular),
607
- lives in its own repo now rather than in `examples/` here, consumed as a real package
608
- (currently via a `file:` dependency, pending an npm publish of both). A real app built on
609
- both lives in [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) —
610
- components, `state<T>`-driven reactivity, a constructor-injected service, and
611
- hash-based routing between two pages, verified against a real DOM via jsdom and in an
612
- actual browser.
716
+ lives in its own repo, published to npm as `kopular` and consumed like any other package
717
+ (no local checkout or `file:` dependency needed). A real app built on both lives in
718
+ [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) — several
719
+ routed pages, `state<T>`-driven reactivity, constructor-injected services via a
720
+ composition root (no DI container), structural-directive equivalents for `*ngIf`/`*ngFor`/
721
+ `*ngSwitch`, and real-URL (History API) routing — verified against a real DOM via jsdom
722
+ and in an actual browser.