kopscript 0.15.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
@@ -238,12 +238,27 @@ limitation, not a semantic one).
238
238
  ```ks
239
239
  number[] xs = [1, 2, 3];
240
240
  xs.Length
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
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
243
243
  xs.Filter((number x) => x > 1)
244
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
245
255
  ```
246
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
+
247
262
  ### Strings
248
263
 
249
264
  ```ks
package/README.md CHANGED
@@ -676,8 +676,9 @@ KopScript's `string` type exposes PascalCase members that map directly onto
676
676
 
677
677
  ### Array stdlib
678
678
 
679
- Arrays expose `.Length`, plus `Map`/`Filter`/`ForEach`/`Push`, using lambdas or any other
680
- 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, ...):
681
682
 
682
683
  ```ks
683
684
  number[] xs = [1, 2, 3, 4];
@@ -685,21 +686,44 @@ string[] labels = xs.Map((number x) => "n" + x); // ["n1", "n2", "n3", "n4"
685
686
  number[] evens = xs.Filter((number x) => x % 2 == 0); // [2, 4]
686
687
  xs.ForEach((number x) => print(x));
687
688
  number[] grown = xs.Push(5); // [1, 2, 3, 4, 5]; xs itself is untouched
688
- ```
689
689
 
690
- `Map`/`Filter`/`ForEach` compile straight to their real `Array.prototype` equivalents.
691
- `Push` is the one departure from JS: it's **non-mutating** (returns a new array; `xs`
692
- itself is unchanged), unlike JS's own `Array.prototype.push` — chosen for consistency with
693
- `Map`/`Filter` (already non-mutating) and because nothing else in KopScript's type system models
694
- aliasing/mutable-reference semantics, so a silently-mutating `Push` would be a surprising
695
- 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]"`.
696
717
 
697
718
  `Map`'s result type is the one genuinely polymorphic piece of the whole language — the
698
719
  result element type is whatever the callback actually returns, not a fixed signature.
699
720
  A plain function reference (`xs.Map(SomeFunction)`) already carries a fully-known type, so
700
721
  that case is exact; an inline expression-bodied lambda (`xs.Map((number x) => ...)`) has
701
722
  its return type inferred from the body. A block-bodied lambda passed to `Map` is a known
702
- 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.
703
727
 
704
728
  ### Control flow
705
729
 
package/dist/checker.js CHANGED
@@ -1714,8 +1714,9 @@ export class Checker {
1714
1714
  return info.returnType;
1715
1715
  }
1716
1716
  if (expr.callee.kind === "MemberExpr") {
1717
- // Array.Map's result type is polymorphic (derived from the callback),
1718
- // 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 —
1719
1720
  // handled here instead, before the generic member/call path below.
1720
1721
  if (expr.callee.property === "Map") {
1721
1722
  const objectType = this.checkExpression(expr.callee.object, scope, ctx);
@@ -1724,6 +1725,13 @@ export class Checker {
1724
1725
  return this.checkArrayMap(expr, objectType.element, scope, ctx);
1725
1726
  }
1726
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
+ }
1727
1735
  const { type: methodType, methodInfo } = this.checkMember(expr.callee, scope, ctx);
1728
1736
  if (methodInfo) {
1729
1737
  this.checkArgs(expr, methodInfo.params, scope, ctx);
@@ -1911,6 +1919,48 @@ export class Checker {
1911
1919
  // nothing else in the type system models. Compiles to `[...arr, x]`.
1912
1920
  return { ...method, params: [elementType], returnType: T.arrayOf(elementType) };
1913
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
+ }
1914
1964
  return null;
1915
1965
  }
1916
1966
  // Array.Map: `arr.Map(f)` where f: (T) => U, result: U[]. U is whatever
@@ -1939,6 +1989,34 @@ export class Checker {
1939
1989
  }
1940
1990
  return T.arrayOf(argType.returnType);
1941
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
+ }
1942
2020
  checkMember(expr, scope, ctx, isAssignTarget = false) {
1943
2021
  const result = this.checkMemberInner(expr, scope, ctx, isAssignTarget);
1944
2022
  const text = result.methodInfo
@@ -2003,12 +2081,13 @@ export class Checker {
2003
2081
  const method = this.arrayMethod(objectType.element, expr.property);
2004
2082
  if (method)
2005
2083
  return { type: method.returnType, methodInfo: method };
2006
- // Map isn't handled here at all — its result type is polymorphic
2007
- // (derived from the callback passed at the call site), which doesn't
2008
- // fit this fixed-signature lookup. checkCall special-cases it before
2009
- // ever reaching checkMember, so `arr.Map(f)` works; `arr.Map` used as
2010
- // a bare value (not called) falls through to this error, same as an
2011
- // 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.
2012
2091
  this.diagnostics.error("KS4080", `Unknown array member '${expr.property}'`, expr.line, expr.col);
2013
2092
  return { type: T.UNKNOWN, methodInfo: null };
2014
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.15.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",