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 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 but are deliberately
79
- scoped down see the Generics section for exactly what's NOT supported there (multiple
80
- type parameters, constraints, generic functions, generic inheritance).
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) // NON-mutating — returns a new array, doesn't modify xs
241
- xs.Map((number x) => x * 2) // -> array of whatever the callback returns
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 functions.** `T Identity<T>(T x)` doesn't parse only classes and interfaces
371
- take type parameters, not free functions/methods themselves (a method *inside* a
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
- and single-generic-base inheritance (no `T : IFoo, IBar` multi-constraints, no generic
546
- functions, no variance, no more than one generic entry per base list — see Generics above
547
- for what *is* supported) · `any`/`unknown` annotations ·
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 functions.** `T Identity<T>(T x) { return x; }` doesn't parse — only classes
583
- and interfaces take `<T>`. Don't reach for this even though every mainstream generic
584
- language supports it.
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 interfaces
26
- (`class Box<T> { public T Value; }`, `class Pair<K, V> { ... }`), each optionally
27
- constrained to an interface (`class Box<T : IComparable> { ... }`), plus generic
28
- inheritance (`class IntBox : Box<number> { }`) — erased at codegen with zero runtime
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`, using lambdas or any other
665
- function-valued expression (a named function, a variable holding one, ...):
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
- `Map`/`Filter`/`ForEach` compile straight to their real `Array.prototype` equivalents.
676
- `Push` is the one departure from JS: it's **non-mutating** (returns a new array; `xs`
677
- itself is unchanged), unlike JS's own `Array.prototype.push` — chosen for consistency with
678
- `Map`/`Filter` (already non-mutating) and because nothing else in KopScript's type system models
679
- aliasing/mutable-reference semantics, so a silently-mutating `Push` would be a surprising
680
- outlier. It compiles to a plain spread (`[...xs, 5]`), not a `.push()` call.
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 (a single
896
- unconstrained, invariant type parameter on classes/interfaces) have both shipped — see
897
- above for both. What generics deliberately doesn't cover: multiple type parameters
898
- (`Map<K, V>`), constraints (`T : IFoo`), generic functions, generic inheritance, and
899
- variance each a real, separable extension rather than a v1 oversight. Also not yet
900
- supported: static auto-properties, interface properties (methods only), and nested
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 = decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col));
883
- const returnType = this.resolveType(decl.returnType, decl.line, decl.col);
884
- this.functions.set(decl.name, { params, returnType });
885
- this.recordHover(decl.line, decl.col, `function ${decl.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
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
- const info = this.functions.get(decl.name);
1086
- const scope = parentScope.child();
1087
- decl.params.forEach((p, i) => scope.declare(p.name, info.params[i], false));
1088
- const returnType = this.resolveBodyReturnType(info.returnType, decl.isAsync, decl.line, decl.col);
1089
- this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
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 type is polymorphic (derived from the callback),
1634
- // which doesn't fit checkMember's fixed-signature methodInfo shape —
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 isn't handled here at all — its result type is polymorphic
1879
- // (derived from the callback passed at the call site), which doesn't
1880
- // fit this fixed-signature lookup. checkCall special-cases it before
1881
- // ever reaching checkMember, so `arr.Map(f)` works; `arr.Map` used as
1882
- // a bare value (not called) falls through to this error, same as an
1883
- // unknown member a known v1 restriction.
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 need nothing beyond the
14
- // rename the callback and everything else already codegens generically.
15
- // Push is handled separately in genCall, since it's non-mutating in KopScript
16
- // (unlike JS's own Array.push) and needs a different call shape entirely.
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
- return `${pad}${prefix}${this.printType(decl.returnType)} ${decl.name}(${this.printParams(decl.params)}) ${this.printBlock(decl.body, indent).trimStart()}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types",
5
5
  "type": "module",
6
6
  "license": "MIT",