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 +21 -0
- package/README.md +588 -0
- package/bin/ks.js +17 -0
- package/dist/ast.js +2 -0
- package/dist/checker.js +1334 -0
- package/dist/cli.js +100 -0
- package/dist/codegen.js +339 -0
- package/dist/diagnostics.js +26 -0
- package/dist/lexer.js +301 -0
- package/dist/modules.js +133 -0
- package/dist/parser.js +995 -0
- package/dist/tokens.js +81 -0
- package/dist/types.js +108 -0
- package/package.json +43 -0
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<T>, 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<T>
|
|
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