kopscript 0.14.0 → 0.16.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 +55 -15
- package/README.md +64 -25
- package/dist/checker.js +224 -17
- package/dist/codegen.js +28 -4
- package/dist/parser.js +11 -1
- package/dist/printer.js +2 -1
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -75,9 +75,10 @@ below). No circular `using` (compile error).
|
|
|
75
75
|
**Does not exist**: `any`/`unknown` as a writable annotation, a `let`/`var` keyword (see
|
|
76
76
|
Declarations below — there isn't one), type inference for declarations (every
|
|
77
77
|
local/`const`/param/field/return type is written out explicitly), union types, tuples,
|
|
78
|
-
structural/duck typing (all typing is nominal). Generics exist
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
structural/duck typing (all typing is nominal). Generics exist and cover multiple type
|
|
79
|
+
parameters, single-interface constraints, generic inheritance, and free generic functions
|
|
80
|
+
— see the Generics section for exactly what's still NOT supported there (variance,
|
|
81
|
+
multiple constraints per parameter, generic methods).
|
|
81
82
|
|
|
82
83
|
## Declarations
|
|
83
84
|
|
|
@@ -237,12 +238,27 @@ limitation, not a semantic one).
|
|
|
237
238
|
```ks
|
|
238
239
|
number[] xs = [1, 2, 3];
|
|
239
240
|
xs.Length
|
|
240
|
-
xs.Push(4)
|
|
241
|
-
xs.Map((number x) => x * 2)
|
|
241
|
+
xs.Push(4) // NON-mutating — returns a new array, doesn't modify xs
|
|
242
|
+
xs.Map((number x) => x * 2) // -> array of whatever the callback returns
|
|
242
243
|
xs.Filter((number x) => x > 1)
|
|
243
244
|
xs.ForEach((number x) => { print(x); })
|
|
245
|
+
xs.Find((number x) => x > 1) // -> T?, null if nothing matches
|
|
246
|
+
xs.FindIndex((number x) => x > 1) // -> number, -1 if nothing matches
|
|
247
|
+
xs.Includes(2) // -> bool
|
|
248
|
+
xs.IndexOf(2) // -> number, -1 if not present
|
|
249
|
+
xs.Sort((number a, number b) => a - b) // NON-mutating, comparator always required
|
|
250
|
+
xs.Reverse() // NON-mutating
|
|
251
|
+
xs.Slice(1, 2) // both bounds always required, no optional params
|
|
252
|
+
xs.Concat(ys) // ys: same element type
|
|
253
|
+
xs.Join(", ") // -> string; number[]/string[]/bool[] element types only
|
|
254
|
+
xs.Reduce((number acc, number x) => acc + x, 0) // (acc, initial) — accumulator type = initial's type
|
|
244
255
|
```
|
|
245
256
|
|
|
257
|
+
`Push`/`Sort`/`Reverse` are non-mutating (return a new array) — the one deliberate
|
|
258
|
+
departure from real JS, where all three mutate in place; everything else here compiles
|
|
259
|
+
straight to its real `Array.prototype` equivalent. `Reduce`'s argument order matches real
|
|
260
|
+
JS (`reducer, initial`, not the other way around).
|
|
261
|
+
|
|
246
262
|
### Strings
|
|
247
263
|
|
|
248
264
|
```ks
|
|
@@ -366,10 +382,30 @@ implemented/extended interface — never more than one) — `class Foo<T> : Box<
|
|
|
366
382
|
IContainer<T>` is a compile error ("has more than one generic entry in its base list") even
|
|
367
383
|
though either half alone would work.
|
|
368
384
|
|
|
385
|
+
**Generic functions**: only free/top-level functions, never a method inside a class (a
|
|
386
|
+
method still only ever uses its enclosing class's own type parameters):
|
|
387
|
+
```ks
|
|
388
|
+
T Identity<T>(T x) { return x; }
|
|
389
|
+
number n = Identity(5); // T inferred as number, from the argument
|
|
390
|
+
string s = Identity("hi"); // T inferred as string, independently
|
|
391
|
+
```
|
|
392
|
+
**No explicit type argument at a call site** — `Identity<number>(5)` isn't valid syntax;
|
|
393
|
+
`T` is always inferred from the actual argument types by structurally unifying each
|
|
394
|
+
declared (possibly abstract) parameter type against its argument's real type — including
|
|
395
|
+
through a nested generic type (`T Unwrap<T>(Box<T> b)` infers `T` from `Box<number>`) or a
|
|
396
|
+
lambda argument's own explicit parameter type (`void UseCallback<T>((T) => void cb)` infers
|
|
397
|
+
`T` from `(number n) => ...`). This is a real grammar constraint, not a missing feature:
|
|
398
|
+
`new Box<number>(...)` disambiguates `<` from a comparison only because `new` is a distinct
|
|
399
|
+
keyword context — a bare call has no such anchor, so `f<T>(x)` would be genuinely ambiguous
|
|
400
|
+
with a chained `<`/`>` comparison. If no argument determines a declared type parameter, or
|
|
401
|
+
two arguments would bind it to conflicting types, that's a compile error naming the
|
|
402
|
+
parameter, not a syntax feature to reach for. A constrained type parameter (`T :
|
|
403
|
+
IComparable`) works exactly like it does on a class — the inferred type must satisfy it,
|
|
404
|
+
checked after inference succeeds.
|
|
405
|
+
|
|
369
406
|
**Does not exist (v1 scope cuts, each deliberate)**:
|
|
370
|
-
- **Generic
|
|
371
|
-
|
|
372
|
-
generic class can use that class's own type parameters freely, same as any other member).
|
|
407
|
+
- **Generic methods.** A class method can't introduce its own new type parameter beyond
|
|
408
|
+
its enclosing class's (generic functions are free-function-only — see above).
|
|
373
409
|
- **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
|
|
374
410
|
|
|
375
411
|
## Nullable types — `T?`
|
|
@@ -541,10 +577,11 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
|
|
|
541
577
|
|
|
542
578
|
## Does not exist (don't reach for these)
|
|
543
579
|
|
|
544
|
-
Generics beyond one or more type parameters (each with at most one interface constraint)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
580
|
+
Generics beyond one or more type parameters (each with at most one interface constraint),
|
|
581
|
+
single-generic-base inheritance, and free (never method-level) generic functions with
|
|
582
|
+
inference-only call sites (no `T : IFoo, IBar` multi-constraints, no explicit
|
|
583
|
+
`Identity<number>(5)` type arguments, no variance, no more than one generic entry per base
|
|
584
|
+
list — see Generics above for what *is* supported) · `any`/`unknown` annotations ·
|
|
548
585
|
a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
|
|
549
586
|
reflection · type inference on declarations · ternary expression · union/tuple types ·
|
|
550
587
|
**object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
|
|
@@ -579,9 +616,12 @@ Real, observed cases where a plausible-looking guess was wrong — not hypotheti
|
|
|
579
616
|
dedicated rewriter, `qualifyThis`, specifically because this isn't automatic.)
|
|
580
617
|
- **No `let`/`var`, ever.** Every local is `Type name = value;` — writing `let x = 5;` or a
|
|
581
618
|
bare `const x = 5;` (missing the type) is a parse error, not a lenient inferred form.
|
|
582
|
-
- **No generic
|
|
583
|
-
|
|
584
|
-
|
|
619
|
+
- **No explicit type argument at a generic function call site.** `Identity<number>(5)`
|
|
620
|
+
doesn't parse — a bare call has no keyword like `new` to disambiguate `<` from a
|
|
621
|
+
comparison, so type arguments are always inferred from the actual arguments instead.
|
|
622
|
+
Just call `Identity(5)`; if inference can't determine a type parameter, restructure the
|
|
623
|
+
call (e.g. an argument that actually mentions the type) rather than reaching for explicit
|
|
624
|
+
syntax that isn't there.
|
|
585
625
|
- **Early-return doesn't narrow a nullable type.** `if (x == null) { return; } print(x.Length);`
|
|
586
626
|
still errors on `x.Length` — narrowing is scope-based (an `if`/`else` block), not
|
|
587
627
|
control-flow/reachability-based. Wrap the rest of the logic in the `if (x != null) { ... }`
|
package/README.md
CHANGED
|
@@ -22,11 +22,11 @@ LLM's context, as opposed to this README's narrative explanation.
|
|
|
22
22
|
explicit `virtual`/`override` dispatch (methods are sealed unless marked `virtual`), and a full
|
|
23
23
|
`public`/`protected`/`private` access model enforced at compile time.
|
|
24
24
|
- **Strongly typed**: every declaration is explicitly typed and checked at compile time.
|
|
25
|
-
- **Generics**: one or more invariant type parameters on classes and
|
|
26
|
-
(`class Box<T> { public T Value; }`, `
|
|
27
|
-
constrained to an interface (`class Box<T : IComparable> { ... }`), plus
|
|
28
|
-
inheritance (`class IntBox : Box<number> { }`) — erased at codegen with zero
|
|
29
|
-
cost, the same way `task<T>`/`state<T>` already are.
|
|
25
|
+
- **Generics**: one or more invariant type parameters on classes, interfaces, and free
|
|
26
|
+
functions (`class Box<T> { public T Value; }`, `T Identity<T>(T x) { return x; }`), each
|
|
27
|
+
optionally constrained to an interface (`class Box<T : IComparable> { ... }`), plus
|
|
28
|
+
generic inheritance (`class IntBox : Box<number> { }`) — erased at codegen with zero
|
|
29
|
+
runtime cost, the same way `task<T>`/`state<T>` already are.
|
|
30
30
|
- **Nullable types with compiler-enforced null-checking**: `T?` (`string?`, `Dog?`,
|
|
31
31
|
`number[]?`) — a `T?` can't be used where a `T` is expected without an `if (x != null)`
|
|
32
32
|
check first (checked statically, not just at runtime), and comparing a value that can
|
|
@@ -258,9 +258,6 @@ inside v1:
|
|
|
258
258
|
- **Invariant.** `Box<Dog>` is **not** assignable to `Box<Animal>` even though `Dog :
|
|
259
259
|
Animal`, and `Pair<number, string>` is not assignable to `Pair<string, number>` — every
|
|
260
260
|
slot must match exactly, in order.
|
|
261
|
-
- **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
|
|
262
|
-
interfaces take type parameters. A method *inside* a generic class can still use that
|
|
263
|
-
class's own type parameters freely; it's just not introducing one of its own.
|
|
264
261
|
|
|
265
262
|
By default a type parameter is fully unconstrained, so you can't call any member on a bare
|
|
266
263
|
`T`/`K`/`V` value inside the generic class's own body — the same restriction C#/Java/
|
|
@@ -299,6 +296,24 @@ down. **v1 limit**: at most one generic entry across a whole base list (the supe
|
|
|
299
296
|
one implemented interface — not several at once) — `class Foo<T> : Box<T>, IContainer<T>`
|
|
300
297
|
is a compile error even though each half would work alone.
|
|
301
298
|
|
|
299
|
+
**Free functions can be generic too** — only free/top-level functions, not class methods
|
|
300
|
+
(a method inside a class still only uses its enclosing class's own type parameters, never
|
|
301
|
+
introduces a new one):
|
|
302
|
+
|
|
303
|
+
```ks
|
|
304
|
+
T Identity<T>(T x) { return x; }
|
|
305
|
+
number n = Identity(5); // T = number, inferred from the argument
|
|
306
|
+
string s = Identity("hi"); // T = string, independently
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**No explicit type argument at the call site** — `Identity<number>(5)` isn't valid syntax;
|
|
310
|
+
`T` is always inferred from the actual argument types. This is a real grammar constraint,
|
|
311
|
+
not a missing feature: `new Box<number>(...)` can disambiguate `<` from a comparison only
|
|
312
|
+
because `new` is a distinct keyword context — a bare call has no such anchor, so
|
|
313
|
+
`f<T>(x)` would be genuinely ambiguous with a chained `<`/`>` comparison. If inference
|
|
314
|
+
can't pin down a type parameter from the arguments given, that's a compile error naming the
|
|
315
|
+
unresolved parameter, not a syntax feature to reach for.
|
|
316
|
+
|
|
302
317
|
See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
|
|
303
318
|
against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`, `Pair<number,
|
|
304
319
|
Pair<string, bool>>`) and generic interfaces used as standalone parameter types both work
|
|
@@ -661,8 +676,9 @@ KopScript's `string` type exposes PascalCase members that map directly onto
|
|
|
661
676
|
|
|
662
677
|
### Array stdlib
|
|
663
678
|
|
|
664
|
-
Arrays expose `.Length`, plus `Map`/`Filter`/`ForEach`/`Push
|
|
665
|
-
|
|
679
|
+
Arrays expose `.Length`, plus `Map`/`Filter`/`ForEach`/`Push`/`Find`/`FindIndex`/
|
|
680
|
+
`Includes`/`IndexOf`/`Sort`/`Reverse`/`Slice`/`Concat`/`Join`/`Reduce`, using lambdas or
|
|
681
|
+
any other function-valued expression (a named function, a variable holding one, ...):
|
|
666
682
|
|
|
667
683
|
```ks
|
|
668
684
|
number[] xs = [1, 2, 3, 4];
|
|
@@ -670,21 +686,44 @@ string[] labels = xs.Map((number x) => "n" + x); // ["n1", "n2", "n3", "n4"
|
|
|
670
686
|
number[] evens = xs.Filter((number x) => x % 2 == 0); // [2, 4]
|
|
671
687
|
xs.ForEach((number x) => print(x));
|
|
672
688
|
number[] grown = xs.Push(5); // [1, 2, 3, 4, 5]; xs itself is untouched
|
|
673
|
-
```
|
|
674
689
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
690
|
+
number? found = xs.Find((number x) => x > 2); // 3, or null if nothing matches
|
|
691
|
+
number idx = xs.FindIndex((number x) => x > 2); // 2, or -1 if nothing matches
|
|
692
|
+
bool has = xs.Includes(3); // true
|
|
693
|
+
number at = xs.IndexOf(3); // 2, or -1 if not present
|
|
694
|
+
|
|
695
|
+
number[] sorted = xs.Sort((number a, number b) => a - b); // new array; xs untouched
|
|
696
|
+
number[] rev = xs.Reverse(); // new array; xs untouched
|
|
697
|
+
number[] mid = xs.Slice(1, 3); // [2, 3]
|
|
698
|
+
number[] joined = xs.Concat([5, 6]); // [1, 2, 3, 4, 5, 6]
|
|
699
|
+
string csv = xs.Join(", "); // "1, 2, 3, 4"
|
|
700
|
+
|
|
701
|
+
number sum = xs.Reduce((number acc, number x) => acc + x, 0); // 10
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`Map`/`Filter`/`ForEach`/`Find`/`FindIndex`/`Includes`/`IndexOf`/`Slice`/`Concat`/`Join`/
|
|
705
|
+
`Reduce` compile straight to their real `Array.prototype` equivalents. `Push`/`Sort`/
|
|
706
|
+
`Reverse` are the departures from JS: all three are **non-mutating** (return a new array;
|
|
707
|
+
the original is unchanged) — unlike JS's own `push`/`sort`/`reverse`, which all mutate in
|
|
708
|
+
place — chosen for consistency and because nothing else in KopScript's type system models
|
|
709
|
+
aliasing/mutable-reference semantics, so a silently-mutating method would be a surprising
|
|
710
|
+
outlier. `Sort`/`Reverse` compile to a spread-copy first (`[...xs].sort(...)`), not a
|
|
711
|
+
direct `.sort()`/`.reverse()` call. `Sort`'s comparator is always required (no
|
|
712
|
+
optional-parameter support in KopScript to fall back on, and it sidesteps real JS
|
|
713
|
+
`Array.sort`'s own well-known footgun — lexicographic-by-default on non-strings). `Slice`'s
|
|
714
|
+
both bounds are likewise always required. `Join` is scoped to primitive element types
|
|
715
|
+
(`number[]`/`string[]`/`bool[]`) only — a class/interface element has no way to customize
|
|
716
|
+
`toString()` in KopScript, so allowing it there would just produce `"[object Object]"`.
|
|
681
717
|
|
|
682
718
|
`Map`'s result type is the one genuinely polymorphic piece of the whole language — the
|
|
683
719
|
result element type is whatever the callback actually returns, not a fixed signature.
|
|
684
720
|
A plain function reference (`xs.Map(SomeFunction)`) already carries a fully-known type, so
|
|
685
721
|
that case is exact; an inline expression-bodied lambda (`xs.Map((number x) => ...)`) has
|
|
686
722
|
its return type inferred from the body. A block-bodied lambda passed to `Map` is a known
|
|
687
|
-
v1 gap — its result type can't be inferred that way yet.
|
|
723
|
+
v1 gap — its result type can't be inferred that way yet. `Reduce` is similarly
|
|
724
|
+
polymorphic, but simpler to infer: its accumulator type comes directly from the `initial`
|
|
725
|
+
argument (`xs.Reduce(reducer, initial)`, matching real JS's own argument order), not from
|
|
726
|
+
inspecting the callback body.
|
|
688
727
|
|
|
689
728
|
### Control flow
|
|
690
729
|
|
|
@@ -892,13 +931,13 @@ into your extensions folder).
|
|
|
892
931
|
|
|
893
932
|
## Status
|
|
894
933
|
|
|
895
|
-
This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (
|
|
896
|
-
|
|
897
|
-
above for
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
functions/classes.
|
|
934
|
+
This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (multiple type
|
|
935
|
+
parameters, single-interface constraints, generic inheritance, and free generic functions)
|
|
936
|
+
have all shipped — see above for the full "Generics" section. What generics still
|
|
937
|
+
deliberately doesn't cover: variance, multiple constraints per parameter, generic methods
|
|
938
|
+
(only free functions), and more than one generic entry per base list — each a real,
|
|
939
|
+
separable extension rather than a v1 oversight. Also not yet supported: static
|
|
940
|
+
auto-properties, interface properties (methods only), and nested functions/classes.
|
|
902
941
|
|
|
903
942
|
`async`/`await`, `task<T>`, and `try`/`catch`/`finally`/`throw` are now in place (see the
|
|
904
943
|
language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
|
package/dist/checker.js
CHANGED
|
@@ -194,7 +194,12 @@ export class Checker {
|
|
|
194
194
|
this.checkClassBody(c);
|
|
195
195
|
}
|
|
196
196
|
registerExternFunction(decl) {
|
|
197
|
+
// extern functions never take a type parameter of their own in v1 —
|
|
198
|
+
// generic functions (see registerFunction) are a real-function-only
|
|
199
|
+
// feature; an extern binding always describes a concrete signature.
|
|
197
200
|
this.functions.set(decl.name, {
|
|
201
|
+
typeParams: [],
|
|
202
|
+
typeParamConstraints: [],
|
|
198
203
|
params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
199
204
|
returnType: this.resolveType(decl.returnType, decl.line, decl.col),
|
|
200
205
|
});
|
|
@@ -504,6 +509,68 @@ export class Checker {
|
|
|
504
509
|
return type;
|
|
505
510
|
}
|
|
506
511
|
}
|
|
512
|
+
// The reverse of substituteTypeParams: derives bindings *from* matching a
|
|
513
|
+
// generic function's declared (abstract) parameter type against one real
|
|
514
|
+
// argument's actual (concrete) type, instead of substituting bindings
|
|
515
|
+
// *into* a type. Structural unification, same recursive shape as
|
|
516
|
+
// substituteTypeParams/typesEqual — whenever `declared` is a TypeParamType
|
|
517
|
+
// for one of `typeParamNames`, records `bindings.set(name, actual)` the
|
|
518
|
+
// first time it's seen; a later argument that would bind the same name to
|
|
519
|
+
// a genuinely different type is recorded in `conflicts` instead of
|
|
520
|
+
// silently overwriting (see checkGenericFunctionCall's own error for
|
|
521
|
+
// that). A structural mismatch elsewhere (e.g. declared is `T[]` but
|
|
522
|
+
// actual isn't an array at all) simply infers nothing from that
|
|
523
|
+
// position — the normal post-substitution assignability check catches
|
|
524
|
+
// the real type error afterward, with a clearer message than unification
|
|
525
|
+
// failing silently here would give.
|
|
526
|
+
inferTypeParamBindings(declared, actual, typeParamNames, bindings, conflicts) {
|
|
527
|
+
if (declared.kind === "typeParam" && typeParamNames.has(declared.name)) {
|
|
528
|
+
const existing = bindings.get(declared.name);
|
|
529
|
+
if (existing) {
|
|
530
|
+
if (!T.typesEqual(existing, actual))
|
|
531
|
+
conflicts.set(declared.name, actual);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
bindings.set(declared.name, actual);
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (declared.kind === "array" && actual.kind === "array") {
|
|
539
|
+
this.inferTypeParamBindings(declared.element, actual.element, typeParamNames, bindings, conflicts);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (declared.kind === "nullable") {
|
|
543
|
+
const actualInner = actual.kind === "nullable" ? actual.inner : actual;
|
|
544
|
+
this.inferTypeParamBindings(declared.inner, actualInner, typeParamNames, bindings, conflicts);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (declared.kind === "task" && actual.kind === "task") {
|
|
548
|
+
this.inferTypeParamBindings(declared.resultType, actual.resultType, typeParamNames, bindings, conflicts);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (declared.kind === "state" && actual.kind === "state") {
|
|
552
|
+
this.inferTypeParamBindings(declared.valueType, actual.valueType, typeParamNames, bindings, conflicts);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if ((declared.kind === "class" || declared.kind === "interface") && declared.kind === actual.kind && declared.name === actual.name) {
|
|
556
|
+
const declaredArgs = declared.typeArgs ?? [];
|
|
557
|
+
const actualArgs = actual.typeArgs ?? [];
|
|
558
|
+
declaredArgs.forEach((d, i) => {
|
|
559
|
+
if (actualArgs[i])
|
|
560
|
+
this.inferTypeParamBindings(d, actualArgs[i], typeParamNames, bindings, conflicts);
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (declared.kind === "function" && actual.kind === "function") {
|
|
565
|
+
declared.params.forEach((d, i) => {
|
|
566
|
+
if (actual.params[i])
|
|
567
|
+
this.inferTypeParamBindings(d, actual.params[i], typeParamNames, bindings, conflicts);
|
|
568
|
+
});
|
|
569
|
+
this.inferTypeParamBindings(declared.returnType, actual.returnType, typeParamNames, bindings, conflicts);
|
|
570
|
+
}
|
|
571
|
+
// Anything else (primitive/enum/void/unknown, or a structural
|
|
572
|
+
// mismatch) has nothing to unify — no-op.
|
|
573
|
+
}
|
|
507
574
|
// Builds the bindings map for a generic reference (a `ClassType`/
|
|
508
575
|
// `InterfaceType`'s own `typeArgs`, zipped against its declared
|
|
509
576
|
// `typeParams`) — the "own bindings" every member-lookup/conformance
|
|
@@ -879,10 +946,18 @@ export class Checker {
|
|
|
879
946
|
}
|
|
880
947
|
}
|
|
881
948
|
registerFunction(decl) {
|
|
882
|
-
const params
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
949
|
+
const { params, returnType } = this.withTypeParamsInScope(decl.typeParams, () => ({
|
|
950
|
+
params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
951
|
+
returnType: this.resolveType(decl.returnType, decl.line, decl.col),
|
|
952
|
+
}));
|
|
953
|
+
this.functions.set(decl.name, {
|
|
954
|
+
typeParams: decl.typeParams.map((p) => p.name),
|
|
955
|
+
typeParamConstraints: decl.typeParams.map((p) => p.constraint),
|
|
956
|
+
params,
|
|
957
|
+
returnType,
|
|
958
|
+
});
|
|
959
|
+
const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
|
|
960
|
+
this.recordHover(decl.line, decl.col, `function ${nameWithTypeParam}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
886
961
|
}
|
|
887
962
|
isSubclass(sub, sup) {
|
|
888
963
|
let current = sub;
|
|
@@ -1082,11 +1157,17 @@ export class Checker {
|
|
|
1082
1157
|
this.checkStatement(stmt, scope, { returnType: T.VOID, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: true });
|
|
1083
1158
|
}
|
|
1084
1159
|
checkFunctionBody(decl, parentScope) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
this.
|
|
1160
|
+
// Same withTypeParamsInScope treatment checkClassBody gives a generic
|
|
1161
|
+
// class's own method bodies — without it, a generic function's type
|
|
1162
|
+
// parameter would resolve in its declared signature but not inside a
|
|
1163
|
+
// local declaration/lambda written in its own body.
|
|
1164
|
+
this.withTypeParamsInScope(decl.typeParams, () => {
|
|
1165
|
+
const info = this.functions.get(decl.name);
|
|
1166
|
+
const scope = parentScope.child();
|
|
1167
|
+
decl.params.forEach((p, i) => scope.declare(p.name, info.params[i], false));
|
|
1168
|
+
const returnType = this.resolveBodyReturnType(info.returnType, decl.isAsync, decl.line, decl.col);
|
|
1169
|
+
this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
|
|
1170
|
+
});
|
|
1090
1171
|
}
|
|
1091
1172
|
checkClassBody(decl) {
|
|
1092
1173
|
// Constructor/method *bodies* run in this same scope registerClass used
|
|
@@ -1626,12 +1707,16 @@ export class Checker {
|
|
|
1626
1707
|
return T.UNKNOWN;
|
|
1627
1708
|
}
|
|
1628
1709
|
this.recordHover(expr.callee.line, expr.callee.col, `function ${expr.callee.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
|
|
1710
|
+
if (info.typeParams.length > 0) {
|
|
1711
|
+
return this.checkGenericFunctionCall(expr, expr.callee.name, info, scope, ctx);
|
|
1712
|
+
}
|
|
1629
1713
|
this.checkArgs(expr, info.params, scope, ctx);
|
|
1630
1714
|
return info.returnType;
|
|
1631
1715
|
}
|
|
1632
1716
|
if (expr.callee.kind === "MemberExpr") {
|
|
1633
|
-
// Array.Map's result
|
|
1634
|
-
//
|
|
1717
|
+
// Array.Map/Reduce's result types are polymorphic (derived from the
|
|
1718
|
+
// callback and, for Reduce, the initial-value argument), which
|
|
1719
|
+
// doesn't fit checkMember's fixed-signature methodInfo shape —
|
|
1635
1720
|
// handled here instead, before the generic member/call path below.
|
|
1636
1721
|
if (expr.callee.property === "Map") {
|
|
1637
1722
|
const objectType = this.checkExpression(expr.callee.object, scope, ctx);
|
|
@@ -1640,6 +1725,13 @@ export class Checker {
|
|
|
1640
1725
|
return this.checkArrayMap(expr, objectType.element, scope, ctx);
|
|
1641
1726
|
}
|
|
1642
1727
|
}
|
|
1728
|
+
if (expr.callee.property === "Reduce") {
|
|
1729
|
+
const objectType = this.checkExpression(expr.callee.object, scope, ctx);
|
|
1730
|
+
if (objectType.kind === "array") {
|
|
1731
|
+
expr.callee.isBuiltin = true;
|
|
1732
|
+
return this.checkArrayReduce(expr, objectType.element, scope, ctx);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1643
1735
|
const { type: methodType, methodInfo } = this.checkMember(expr.callee, scope, ctx);
|
|
1644
1736
|
if (methodInfo) {
|
|
1645
1737
|
this.checkArgs(expr, methodInfo.params, scope, ctx);
|
|
@@ -1678,6 +1770,50 @@ export class Checker {
|
|
|
1678
1770
|
}
|
|
1679
1771
|
});
|
|
1680
1772
|
}
|
|
1773
|
+
// A generic function call has no explicit type-argument syntax (see
|
|
1774
|
+
// README's "Generics" — a real grammar-ambiguity reason, `f<T>(x)` is
|
|
1775
|
+
// indistinguishable from a chained comparison with no keyword like `new`
|
|
1776
|
+
// to disambiguate it), so every type parameter is inferred from the
|
|
1777
|
+
// actual argument types instead. Checks argument count first (an arity
|
|
1778
|
+
// mismatch would make inference itself meaningless), infers bindings by
|
|
1779
|
+
// unifying each declared (abstract) parameter type against its actual
|
|
1780
|
+
// argument's real type (see inferTypeParamBindings), then re-runs the
|
|
1781
|
+
// normal assignability check with the now-concrete substituted parameter
|
|
1782
|
+
// types — catching anything structural unification alone wouldn't (e.g.
|
|
1783
|
+
// an argument assignable to, but not identical to, its inferred slot).
|
|
1784
|
+
checkGenericFunctionCall(expr, functionName, info, scope, ctx) {
|
|
1785
|
+
if (expr.args.length !== info.params.length) {
|
|
1786
|
+
this.diagnostics.error("KS4098", `Expected ${info.params.length} argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1787
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1788
|
+
return T.UNKNOWN;
|
|
1789
|
+
}
|
|
1790
|
+
const actualArgTypes = expr.args.map((a) => this.checkExpression(a, scope, ctx));
|
|
1791
|
+
const typeParamNames = new Set(info.typeParams);
|
|
1792
|
+
const bindings = new Map();
|
|
1793
|
+
const conflicts = new Map();
|
|
1794
|
+
info.params.forEach((declared, i) => this.inferTypeParamBindings(declared, actualArgTypes[i], typeParamNames, bindings, conflicts));
|
|
1795
|
+
for (const [name, conflictingType] of conflicts) {
|
|
1796
|
+
this.diagnostics.error("KS4099", `Type parameter '${name}' inferred as both '${T.typeToString(bindings.get(name))}' and '${T.typeToString(conflictingType)}' from different arguments — conflicting types`, expr.line, expr.col);
|
|
1797
|
+
}
|
|
1798
|
+
const unresolved = info.typeParams.filter((name) => !bindings.has(name));
|
|
1799
|
+
if (unresolved.length > 0) {
|
|
1800
|
+
this.diagnostics.error("KS4100", `Cannot infer type parameter${unresolved.length === 1 ? "" : "s"} '${unresolved.join("', '")}' for '${functionName}' — no argument determines ${unresolved.length === 1 ? "it" : "them"}`, expr.line, expr.col);
|
|
1801
|
+
return T.UNKNOWN;
|
|
1802
|
+
}
|
|
1803
|
+
info.typeParams.forEach((name, i) => {
|
|
1804
|
+
const constraint = info.typeParamConstraints[i];
|
|
1805
|
+
if (constraint)
|
|
1806
|
+
this.checkConstraintSatisfied(bindings.get(name), constraint, name, expr.line, expr.col);
|
|
1807
|
+
});
|
|
1808
|
+
const substitutedParams = info.params.map((p) => this.substituteTypeParams(p, bindings));
|
|
1809
|
+
expr.args.forEach((arg, i) => {
|
|
1810
|
+
const expected = substitutedParams[i];
|
|
1811
|
+
if (!this.isAssignableType(actualArgTypes[i], expected)) {
|
|
1812
|
+
this.diagnostics.error("KS4101", `Argument ${i + 1} has type '${T.typeToString(actualArgTypes[i])}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
return this.substituteTypeParams(info.returnType, bindings);
|
|
1816
|
+
}
|
|
1681
1817
|
checkNew(expr, scope, ctx) {
|
|
1682
1818
|
if (this.interfaces.has(expr.className)) {
|
|
1683
1819
|
this.diagnostics.error("KS4066", `Cannot instantiate interface '${expr.className}'`, expr.line, expr.col);
|
|
@@ -1783,6 +1919,48 @@ export class Checker {
|
|
|
1783
1919
|
// nothing else in the type system models. Compiles to `[...arr, x]`.
|
|
1784
1920
|
return { ...method, params: [elementType], returnType: T.arrayOf(elementType) };
|
|
1785
1921
|
}
|
|
1922
|
+
if (name === "Find") {
|
|
1923
|
+
// `null` for "no match" rather than throwing/needing a sentinel —
|
|
1924
|
+
// the natural fit given nullable types already exist.
|
|
1925
|
+
return { ...method, params: [T.functionType([elementType], T.BOOL)], returnType: T.nullableOf(elementType) };
|
|
1926
|
+
}
|
|
1927
|
+
if (name === "FindIndex" || name === "IndexOf") {
|
|
1928
|
+
// -1 for "not found", matching real JS — no nullable number needed,
|
|
1929
|
+
// and keeps codegen a plain passthrough with no wrapping/unwrapping.
|
|
1930
|
+
return { ...method, params: name === "FindIndex" ? [T.functionType([elementType], T.BOOL)] : [elementType], returnType: T.NUMBER };
|
|
1931
|
+
}
|
|
1932
|
+
if (name === "Includes") {
|
|
1933
|
+
return { ...method, params: [elementType], returnType: T.BOOL };
|
|
1934
|
+
}
|
|
1935
|
+
if (name === "Sort") {
|
|
1936
|
+
// No optional-parameter support in KopScript, so the comparator is
|
|
1937
|
+
// always required — deliberately avoids real JS Array.sort's own
|
|
1938
|
+
// well-known footgun (lexicographic-by-default on non-strings) by
|
|
1939
|
+
// never having an implicit comparator to fall back to. Non-mutating,
|
|
1940
|
+
// like Push — see genCall's own comment for the codegen shape.
|
|
1941
|
+
return { ...method, params: [T.functionType([elementType, elementType], T.NUMBER)], returnType: T.arrayOf(elementType) };
|
|
1942
|
+
}
|
|
1943
|
+
if (name === "Reverse") {
|
|
1944
|
+
return { ...method, params: [], returnType: T.arrayOf(elementType) };
|
|
1945
|
+
}
|
|
1946
|
+
if (name === "Slice") {
|
|
1947
|
+
// Both bounds always required, same "no optional params" reasoning
|
|
1948
|
+
// as Sort's always-required comparator.
|
|
1949
|
+
return { ...method, params: [T.NUMBER, T.NUMBER], returnType: T.arrayOf(elementType) };
|
|
1950
|
+
}
|
|
1951
|
+
if (name === "Concat") {
|
|
1952
|
+
return { ...method, params: [T.arrayOf(elementType)], returnType: T.arrayOf(elementType) };
|
|
1953
|
+
}
|
|
1954
|
+
if (name === "Join") {
|
|
1955
|
+
// Real JS Array.join stringifies every element via implicit toString
|
|
1956
|
+
// — fine for primitives (numbers/bools stringify sensibly), a real
|
|
1957
|
+
// footgun for a class/interface element with no toString override
|
|
1958
|
+
// ("[object Object]"), which KopScript has no way to customize.
|
|
1959
|
+
// Scoped to primitive element types only, a deliberate v1 cut.
|
|
1960
|
+
if (elementType.kind !== "string" && elementType.kind !== "number" && elementType.kind !== "bool")
|
|
1961
|
+
return null;
|
|
1962
|
+
return { ...method, params: [T.STRING], returnType: T.STRING };
|
|
1963
|
+
}
|
|
1786
1964
|
return null;
|
|
1787
1965
|
}
|
|
1788
1966
|
// Array.Map: `arr.Map(f)` where f: (T) => U, result: U[]. U is whatever
|
|
@@ -1811,6 +1989,34 @@ export class Checker {
|
|
|
1811
1989
|
}
|
|
1812
1990
|
return T.arrayOf(argType.returnType);
|
|
1813
1991
|
}
|
|
1992
|
+
// Array.Reduce: `arr.Reduce(reducer, initial)` where `reducer: (U, T) =>
|
|
1993
|
+
// U`, matching real JS Array.prototype.reduce's own argument order.
|
|
1994
|
+
// Simpler than Map's own inference — U comes directly from `initial`'s
|
|
1995
|
+
// checked type, no partial-lambda-type inference needed at all.
|
|
1996
|
+
checkArrayReduce(expr, elementType, scope, ctx) {
|
|
1997
|
+
if (expr.args.length !== 2) {
|
|
1998
|
+
this.diagnostics.error("KS4102", `Reduce expects exactly 2 arguments, got ${expr.args.length}`, expr.line, expr.col);
|
|
1999
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
2000
|
+
return T.UNKNOWN;
|
|
2001
|
+
}
|
|
2002
|
+
const [reducerArg, initialArg] = expr.args;
|
|
2003
|
+
const accumulatorType = this.checkExpression(initialArg, scope, ctx);
|
|
2004
|
+
const expectedReducerType = T.functionType([accumulatorType, elementType], accumulatorType);
|
|
2005
|
+
const reducerType = this.checkExpressionExpecting(reducerArg, expectedReducerType, scope, ctx);
|
|
2006
|
+
if (reducerType.kind !== "function") {
|
|
2007
|
+
if (reducerType.kind !== "unknown") {
|
|
2008
|
+
this.diagnostics.error("KS4103", `Reduce expects a function as its first argument, got '${T.typeToString(reducerType)}'`, reducerArg.line, reducerArg.col);
|
|
2009
|
+
}
|
|
2010
|
+
return accumulatorType;
|
|
2011
|
+
}
|
|
2012
|
+
const paramsOk = reducerType.params.length === 2 &&
|
|
2013
|
+
this.isAssignableType(accumulatorType, reducerType.params[0]) &&
|
|
2014
|
+
this.isAssignableType(elementType, reducerType.params[1]);
|
|
2015
|
+
if (!paramsOk || !this.isAssignableType(reducerType.returnType, accumulatorType)) {
|
|
2016
|
+
this.diagnostics.error("KS4104", `Reduce callback must take '(${T.typeToString(accumulatorType)}, ${T.typeToString(elementType)})' and return '${T.typeToString(accumulatorType)}', got '${T.typeToString(reducerType)}'`, reducerArg.line, reducerArg.col);
|
|
2017
|
+
}
|
|
2018
|
+
return accumulatorType;
|
|
2019
|
+
}
|
|
1814
2020
|
checkMember(expr, scope, ctx, isAssignTarget = false) {
|
|
1815
2021
|
const result = this.checkMemberInner(expr, scope, ctx, isAssignTarget);
|
|
1816
2022
|
const text = result.methodInfo
|
|
@@ -1875,12 +2081,13 @@ export class Checker {
|
|
|
1875
2081
|
const method = this.arrayMethod(objectType.element, expr.property);
|
|
1876
2082
|
if (method)
|
|
1877
2083
|
return { type: method.returnType, methodInfo: method };
|
|
1878
|
-
// Map
|
|
1879
|
-
// (derived from the callback
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
2084
|
+
// Map/Reduce aren't handled here at all — their result types are
|
|
2085
|
+
// polymorphic (derived from the callback, and for Reduce the initial
|
|
2086
|
+
// value, passed at the call site), which doesn't fit this
|
|
2087
|
+
// fixed-signature lookup. checkCall special-cases both before ever
|
|
2088
|
+
// reaching checkMember, so `arr.Map(f)`/`arr.Reduce(f, init)` work;
|
|
2089
|
+
// either used as a bare value (not called) falls through to this
|
|
2090
|
+
// error, same as an unknown member — a known v1 restriction.
|
|
1884
2091
|
this.diagnostics.error("KS4080", `Unknown array member '${expr.property}'`, expr.line, expr.col);
|
|
1885
2092
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1886
2093
|
}
|
package/dist/codegen.js
CHANGED
|
@@ -10,10 +10,15 @@ function countNewlines(text) {
|
|
|
10
10
|
// blind syntactic rename (codegen has no type information, so this fires on
|
|
11
11
|
// any member access with a matching name, string/array/user-class alike;
|
|
12
12
|
// same pre-existing risk as any of the entries below, going back to the
|
|
13
|
-
// original string stdlib). Map/Filter/ForEach
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// original string stdlib). Map/Filter/ForEach/Find/FindIndex/Includes/
|
|
14
|
+
// IndexOf/Slice/Concat/Join/Reduce need nothing beyond the rename — the
|
|
15
|
+
// callback and everything else already codegens generically, and every one
|
|
16
|
+
// of these is already non-mutating in real JS, so a plain passthrough is
|
|
17
|
+
// correct as-is. Push/Sort/Reverse are handled separately in genCall
|
|
18
|
+
// instead: Push because it's non-mutating in KopScript (unlike JS's own
|
|
19
|
+
// Array.push) and needs a different call shape entirely; Sort/Reverse for
|
|
20
|
+
// the same reason — JS's own versions mutate in place, and KopScript's
|
|
21
|
+
// don't, matching Push's own non-mutating convention.
|
|
17
22
|
const MEMBER_METHOD_MAP = {
|
|
18
23
|
Contains: "includes",
|
|
19
24
|
StartsWith: "startsWith",
|
|
@@ -26,6 +31,14 @@ const MEMBER_METHOD_MAP = {
|
|
|
26
31
|
Map: "map",
|
|
27
32
|
Filter: "filter",
|
|
28
33
|
ForEach: "forEach",
|
|
34
|
+
Find: "find",
|
|
35
|
+
FindIndex: "findIndex",
|
|
36
|
+
Includes: "includes",
|
|
37
|
+
IndexOf: "indexOf",
|
|
38
|
+
Slice: "slice",
|
|
39
|
+
Concat: "concat",
|
|
40
|
+
Join: "join",
|
|
41
|
+
Reduce: "reduce",
|
|
29
42
|
};
|
|
30
43
|
const BINARY_OP_MAP = {
|
|
31
44
|
"+": "+",
|
|
@@ -461,6 +474,17 @@ export class CodeGenerator {
|
|
|
461
474
|
if (expr.callee.kind === "MemberExpr" && expr.callee.isBuiltin && expr.callee.property === "Push" && expr.args.length === 1) {
|
|
462
475
|
return `[...${this.genExpr(expr.callee.object)}, ${this.genExpr(expr.args[0])}]`;
|
|
463
476
|
}
|
|
477
|
+
// Sort/Reverse are non-mutating in KopScript too, same reasoning as
|
|
478
|
+
// Push — JS's own Array.prototype.sort/reverse both mutate in place
|
|
479
|
+
// *and* return the same array, so a plain rename would silently make
|
|
480
|
+
// KopScript code alias its own "immutable" array. Copy first (`[...arr]`),
|
|
481
|
+
// then call the real JS method on the copy.
|
|
482
|
+
if (expr.callee.kind === "MemberExpr" && expr.callee.isBuiltin && expr.callee.property === "Sort" && expr.args.length === 1) {
|
|
483
|
+
return `[...${this.genExpr(expr.callee.object)}].sort(${this.genExpr(expr.args[0])})`;
|
|
484
|
+
}
|
|
485
|
+
if (expr.callee.kind === "MemberExpr" && expr.callee.isBuiltin && expr.callee.property === "Reverse" && expr.args.length === 0) {
|
|
486
|
+
return `[...${this.genExpr(expr.callee.object)}].reverse()`;
|
|
487
|
+
}
|
|
464
488
|
return `${this.genExpr(expr.callee)}(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
|
|
465
489
|
}
|
|
466
490
|
genMember(expr) {
|
package/dist/parser.js
CHANGED
|
@@ -198,14 +198,24 @@ export class Parser {
|
|
|
198
198
|
const type = this.parseType();
|
|
199
199
|
const nameTok = this.consume(TokenKind.Identifier, "Expected name");
|
|
200
200
|
const name = nameTok.lexeme;
|
|
201
|
+
// `T Identity<T>(T x) { ... }` — a free function's own type-param list,
|
|
202
|
+
// between its name and `(`. Only meaningful for the function branch
|
|
203
|
+
// below; a local/top-level variable declaration never has `<` right
|
|
204
|
+
// after its name, so trying this first costs nothing in that case
|
|
205
|
+
// (parseTypeParamList returns immediately without consuming anything
|
|
206
|
+
// when `<` isn't there).
|
|
207
|
+
const typeParams = this.parseTypeParamList();
|
|
201
208
|
if (this.check(TokenKind.LParen)) {
|
|
202
209
|
const params = this.parseParamList();
|
|
203
210
|
const body = this.parseBlock();
|
|
204
|
-
return { kind: "FunctionDecl", isExported, isAsync, name, params, returnType: type, body, line: start.line, col: start.col };
|
|
211
|
+
return { kind: "FunctionDecl", isExported, isAsync, name, typeParams, params, returnType: type, body, line: start.line, col: start.col };
|
|
205
212
|
}
|
|
206
213
|
if (isAsync) {
|
|
207
214
|
this.diagnostics.error("KS2006", "'async' cannot modify a variable declaration", start.line, start.col);
|
|
208
215
|
}
|
|
216
|
+
if (typeParams.length > 0) {
|
|
217
|
+
this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", start.line, start.col);
|
|
218
|
+
}
|
|
209
219
|
this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
|
|
210
220
|
const init = this.parseExpression();
|
|
211
221
|
this.consume(TokenKind.Semicolon, "Expected ';' after variable declaration");
|
package/dist/printer.js
CHANGED
|
@@ -208,7 +208,8 @@ export class Printer {
|
|
|
208
208
|
printFunction(decl, indent) {
|
|
209
209
|
const pad = indentStr(indent);
|
|
210
210
|
const prefix = `${decl.isExported ? "" : "private "}${decl.isAsync ? "async " : ""}`;
|
|
211
|
-
|
|
211
|
+
const nameWithTypeParam = this.printTypeParamName(decl);
|
|
212
|
+
return `${pad}${prefix}${this.printType(decl.returnType)} ${nameWithTypeParam}(${this.printParams(decl.params)}) ${this.printBlock(decl.body, indent).trimStart()}`;
|
|
212
213
|
}
|
|
213
214
|
// Canonical member order: fields, then properties, then the constructor,
|
|
214
215
|
// then methods — regardless of how the original source interleaved them.
|
package/package.json
CHANGED