kopscript 0.15.0 → 0.17.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
@@ -525,11 +540,16 @@ class Counter : Component {
525
540
  - One `template` per class; a class with both `template` and a hand-written `Render()` is a
526
541
  compile error.
527
542
  - Desugars, between parsing and checking, into the exact same `MethodDecl` AST a
528
- hand-written `Render()` would produce — no runtime template engine, no virtual DOM.
529
- `{{ }}`/binding contents are real KopScript, parsed and type-checked normally.
530
- - Bindings: `{{ expr }}` (text interpolation, `InterpolatedStringLiteral`), `(event)="stmt"`
531
- (→ `addEventListener`), `[prop]="expr"` (→ plain assignment, checked like any member
532
- assignment), static `attr="..."` (`class` aliases to `className`).
543
+ hand-written `Render()` would produce — building a `VElement` tree as data, no runtime
544
+ template engine, no diffing of its own (Kopular's own `Component` base class does that
545
+ against the tree this produces). `{{ }}`/binding contents are real KopScript, parsed and
546
+ type-checked normally.
547
+ - Bindings: `{{ expr }}` (text interpolation, `VElement.TextContent = InterpolatedStringLiteral`),
548
+ `(event)="stmt"` (→ assignment to one of `VElement`'s four named event fields —
549
+ `OnClick`/`OnInput`/`OnBlur`/`OnChange`; any other event name is a compile error),
550
+ `[prop]="expr"` (→ a plain field assignment for `id`/`className`/`value`, or
551
+ `VElement.SetAttr("prop", expr)` for anything else), static `attr="..."` (`class` aliases
552
+ to `className`).
533
553
  - Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
534
554
  `for..in`; the element type is explicit — no inference, same stance as Generics).
535
555
  At most one structural directive per element.
@@ -541,9 +561,9 @@ class Counter : Component {
541
561
  element children under one element. No two-way binding, no pipes, no stacked directives.
542
562
  - `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
543
563
  - Layering note: `template from` is kopscript grammar, but what it desugars *to*
544
- (`document.createElement`/`.appendChild`/`.textContent`/`.addEventListener`) assumes
545
- Kopular's `dom.ks` DOM surface specifically — a deliberate, documented coupling, not a
546
- generic pluggable target.
564
+ (`VElement.Create`/`.AppendChild`/`.TextContent`/`.SetAttr`/the named `On*` event fields)
565
+ assumes Kopular's `velement.ks` surface specifically — a deliberate, documented coupling,
566
+ not a generic pluggable target.
547
567
 
548
568
  ## Keywords (reserved, lowercase, exact match)
549
569
 
package/README.md CHANGED
@@ -538,12 +538,12 @@ class Counter : Component {
538
538
  this.Count.Subscribe((number v) => this.Update());
539
539
  }
540
540
 
541
- public override Element Render() {
542
- Element button = document.createElement("button");
543
- button.textContent = "Count: " + this.Count.Value;
544
- button.addEventListener("click", (Event e) => {
541
+ public override VElement Render() {
542
+ VElement button = VElement.Create("button");
543
+ button.TextContent = "Count: " + this.Count.Value;
544
+ button.OnClick = (Event e) => {
545
545
  this.Count.Value = this.Count.Value + 1; // Update() fires automatically
546
- });
546
+ };
547
547
  return button;
548
548
  }
549
549
  }
@@ -587,11 +587,13 @@ class Counter : Component {
587
587
  This compiles to exactly the `Render()` method you'd otherwise write by hand — the
588
588
  template compiler is a pass that runs between parsing and type-checking, turning the
589
589
  markup into ordinary `MethodDecl`/statement/expression AST nodes and splicing the result
590
- into the class before checking ever runs. There's no separate runtime template engine, no
591
- virtual DOM diffing, and no interpreted expression language: `{{ Count.Value }}` and
592
- `(click)="Increment()"` contain real KopScript, parsed and type-checked exactly like
593
- anything else in the file, with errors reported at their real position in the `.html`
594
- file, not the `.ks` file.
590
+ into the class before checking ever runs. There's no separate runtime template engine and
591
+ no interpreted expression language: `{{ Count.Value }}` and `(click)="Increment()"`
592
+ contain real KopScript, parsed and type-checked exactly like anything else in the file,
593
+ with errors reported at their real position in the `.html` file, not the `.ks` file. The
594
+ compiler itself does no diffing — it just builds a `VElement` tree as data; Kopular's own
595
+ `Component` base class is what diffs that tree against the previous render and patches
596
+ real DOM (see Kopular's own README/CHANGELOG for that engine).
595
597
 
596
598
  A class may have a `template` or a hand-written `Render()`, never both — that's a compile
597
599
  error. `ks watch` also tracks the referenced `.html` file, so editing markup alone
@@ -601,10 +603,10 @@ Supported bindings and directives:
601
603
 
602
604
  | Syntax | Desugars to |
603
605
  | --------------------------- | --------------------------------------------------------- |
604
- | `{{ expr }}` (in text) | `el.textContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
605
- | `(event)="stmt"` | `el.addEventListener("event", (Event e) => { stmt });` |
606
- | `[prop]="expr"` | `el.prop = expr;` a plain assignment, checked like any other |
607
- | `class="..."` (static) | `el.className = "...";` (aliased, since `class` is a KopScript keyword) |
606
+ | `{{ expr }}` (in text) | `el.TextContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
607
+ | `(event)="stmt"` | `el.OnEvent = (Event e) => { stmt };` — `event` must be one of `click`/`input`/`blur`/`change`, `VElement`'s own fixed set of named event fields |
608
+ | `[prop]="expr"` | `el.Prop = expr;` for `id`/`className`/`value` (`VElement`'s own named fields); `el.SetAttr("prop", expr);` for anything else |
609
+ | `class="..."` (static) | `el.ClassName = "...";` (aliased, since `class` is a KopScript keyword) |
608
610
  | `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
609
611
  | `*for="Type var of expr"` | a real `for (Type var in expr) { ... }` — the element type is explicit, matching KopScript's no-inference stance elsewhere (Generics, Nullable types) |
610
612
 
@@ -623,9 +625,10 @@ two-way binding, no pipes, no stacking two structural directives on one element.
623
625
 
624
626
  **A deliberate layering note**: the `template from` syntax lives in kopscript's own
625
627
  grammar (Kopular can't extend a language it doesn't own), but what it desugars *to* —
626
- `document.createElement`, `.appendChild`, `.textContent`, `.addEventListener` — assumes
627
- exactly the DOM surface [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
628
- `dom.ks` declares. That's a real coupling from the compiler to one specific consumer,
628
+ `VElement.Create`, `.AppendChild`, `.TextContent`, `.SetAttr`, the named `On*` event
629
+ fields — assumes exactly the surface
630
+ [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
631
+ `velement.ks` declares. That's a real coupling from the compiler to one specific consumer,
629
632
  accepted deliberately rather than building a generic pluggable desugaring-target system
630
633
  for a hypothetical second framework that doesn't exist today. If one ever does, that's the
631
634
  point to generalize this.
@@ -676,8 +679,9 @@ KopScript's `string` type exposes PascalCase members that map directly onto
676
679
 
677
680
  ### Array stdlib
678
681
 
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, ...):
682
+ Arrays expose `.Length`, plus `Map`/`Filter`/`ForEach`/`Push`/`Find`/`FindIndex`/
683
+ `Includes`/`IndexOf`/`Sort`/`Reverse`/`Slice`/`Concat`/`Join`/`Reduce`, using lambdas or
684
+ any other function-valued expression (a named function, a variable holding one, ...):
681
685
 
682
686
  ```ks
683
687
  number[] xs = [1, 2, 3, 4];
@@ -685,21 +689,44 @@ string[] labels = xs.Map((number x) => "n" + x); // ["n1", "n2", "n3", "n4"
685
689
  number[] evens = xs.Filter((number x) => x % 2 == 0); // [2, 4]
686
690
  xs.ForEach((number x) => print(x));
687
691
  number[] grown = xs.Push(5); // [1, 2, 3, 4, 5]; xs itself is untouched
688
- ```
689
692
 
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.
693
+ number? found = xs.Find((number x) => x > 2); // 3, or null if nothing matches
694
+ number idx = xs.FindIndex((number x) => x > 2); // 2, or -1 if nothing matches
695
+ bool has = xs.Includes(3); // true
696
+ number at = xs.IndexOf(3); // 2, or -1 if not present
697
+
698
+ number[] sorted = xs.Sort((number a, number b) => a - b); // new array; xs untouched
699
+ number[] rev = xs.Reverse(); // new array; xs untouched
700
+ number[] mid = xs.Slice(1, 3); // [2, 3]
701
+ number[] joined = xs.Concat([5, 6]); // [1, 2, 3, 4, 5, 6]
702
+ string csv = xs.Join(", "); // "1, 2, 3, 4"
703
+
704
+ number sum = xs.Reduce((number acc, number x) => acc + x, 0); // 10
705
+ ```
706
+
707
+ `Map`/`Filter`/`ForEach`/`Find`/`FindIndex`/`Includes`/`IndexOf`/`Slice`/`Concat`/`Join`/
708
+ `Reduce` compile straight to their real `Array.prototype` equivalents. `Push`/`Sort`/
709
+ `Reverse` are the departures from JS: all three are **non-mutating** (return a new array;
710
+ the original is unchanged) — unlike JS's own `push`/`sort`/`reverse`, which all mutate in
711
+ place — chosen for consistency and because nothing else in KopScript's type system models
712
+ aliasing/mutable-reference semantics, so a silently-mutating method would be a surprising
713
+ outlier. `Sort`/`Reverse` compile to a spread-copy first (`[...xs].sort(...)`), not a
714
+ direct `.sort()`/`.reverse()` call. `Sort`'s comparator is always required (no
715
+ optional-parameter support in KopScript to fall back on, and it sidesteps real JS
716
+ `Array.sort`'s own well-known footgun — lexicographic-by-default on non-strings). `Slice`'s
717
+ both bounds are likewise always required. `Join` is scoped to primitive element types
718
+ (`number[]`/`string[]`/`bool[]`) only — a class/interface element has no way to customize
719
+ `toString()` in KopScript, so allowing it there would just produce `"[object Object]"`.
696
720
 
697
721
  `Map`'s result type is the one genuinely polymorphic piece of the whole language — the
698
722
  result element type is whatever the callback actually returns, not a fixed signature.
699
723
  A plain function reference (`xs.Map(SomeFunction)`) already carries a fully-known type, so
700
724
  that case is exact; an inline expression-bodied lambda (`xs.Map((number x) => ...)`) has
701
725
  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.
726
+ v1 gap — its result type can't be inferred that way yet. `Reduce` is similarly
727
+ polymorphic, but simpler to infer: its accumulator type comes directly from the `initial`
728
+ argument (`xs.Reduce(reducer, initial)`, matching real JS's own argument order), not from
729
+ inspecting the callback body.
703
730
 
704
731
  ### Control flow
705
732
 
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) {
@@ -1,6 +1,16 @@
1
- const ELEMENT_TYPE = { kind: "NamedType", name: "Element", typeArgs: null, line: 0, col: 0 };
1
+ const VELEMENT_TYPE = { kind: "NamedType", name: "VElement", typeArgs: null, line: 0, col: 0 };
2
2
  const VOID_TYPE = { kind: "NamedType", name: "void", typeArgs: null, line: 0, col: 0 };
3
3
  const EVENT_TYPE = { kind: "NamedType", name: "Event", typeArgs: null, line: 0, col: 0 };
4
+ // The only attrs/prop bindings with a real named VElement field — anything
5
+ // else goes through SetAttr instead (see buildAttrAssignment). Keys are
6
+ // post-alias names (see template_parser.ts's PROPERTY_ALIASES — "class" is
7
+ // already "className" by the time it reaches here).
8
+ const KNOWN_FIELD_NAMES = { id: "Id", className: "ClassName", value: "Value" };
9
+ // VElement's own fixed, named event slots (see velement.ks) — there's no
10
+ // generic addEventListener on VElement (event handlers are part of the
11
+ // VNode's own data, not wired against a live DOM node), so a template can
12
+ // only bind one of these four.
13
+ const KNOWN_EVENT_NAMES = { click: "OnClick", input: "OnInput", blur: "OnBlur", change: "OnChange" };
4
14
  function ident(name, at) {
5
15
  return { kind: "Identifier", name, line: at.line, col: at.col };
6
16
  }
@@ -20,7 +30,7 @@ function assign(target, value, at) {
20
30
  return exprStatement({ kind: "AssignExpr", target, value, line: at.line, col: at.col }, at);
21
31
  }
22
32
  function varDecl(name, init, at) {
23
- return { kind: "VarDecl", isConst: false, name, nameLine: at.line, nameCol: at.col, type: ELEMENT_TYPE, init, line: at.line, col: at.col };
33
+ return { kind: "VarDecl", isConst: false, name, nameLine: at.line, nameCol: at.col, type: VELEMENT_TYPE, init, line: at.line, col: at.col };
24
34
  }
25
35
  function block(statements, at) {
26
36
  return { kind: "Block", statements, line: at.line, col: at.col };
@@ -181,7 +191,7 @@ export class TemplateCompiler {
181
191
  nameLine: templateDeclAt.line,
182
192
  nameCol: templateDeclAt.col,
183
193
  params: [],
184
- returnType: ELEMENT_TYPE,
194
+ returnType: VELEMENT_TYPE,
185
195
  body: block(statements, templateDeclAt),
186
196
  line: templateDeclAt.line,
187
197
  col: templateDeclAt.col,
@@ -200,14 +210,19 @@ export class TemplateCompiler {
200
210
  const varName = this.freshVar();
201
211
  const statements = [];
202
212
  const self = ident(varName, at);
203
- statements.push(varDecl(varName, call(member(ident("document", at), "createElement", at), [stringLiteral(node.tag, at)], at), at));
213
+ statements.push(varDecl(varName, call(member(ident("VElement", at), "Create", at), [stringLiteral(node.tag, at)], at), at));
204
214
  for (const attr of node.staticAttrs)
205
- statements.push(assign(member(self, attr.name, attr), stringLiteral(attr.value, attr), attr));
215
+ statements.push(this.buildAttrAssignment(self, attr.name, stringLiteral(attr.value, attr), attr));
206
216
  for (const bind of node.propBindings) {
207
217
  const value = this.resolve(bind.value, localScope);
208
- statements.push(assign(member(self, bind.name, bind), value, bind));
218
+ statements.push(this.buildAttrAssignment(self, bind.name, value, bind));
209
219
  }
210
220
  for (const bind of node.eventBindings) {
221
+ const fieldName = KNOWN_EVENT_NAMES[bind.name];
222
+ if (!fieldName) {
223
+ this.diagnostics.error("KS5016", `Unsupported event binding '(${bind.name})' — a template can only bind (click), (input), (blur), or (change), the same fixed set of named event fields VElement itself has`, bind.line, bind.col);
224
+ continue;
225
+ }
211
226
  const handler = this.resolve(bind.handler, localScope);
212
227
  const handlerParam = { name: "e", type: EVENT_TYPE };
213
228
  const lambda = {
@@ -217,7 +232,7 @@ export class TemplateCompiler {
217
232
  line: bind.line,
218
233
  col: bind.col,
219
234
  };
220
- statements.push(exprStatement(call(member(self, "addEventListener", bind), [stringLiteral(bind.name, bind), lambda], bind), bind));
235
+ statements.push(assign(member(self, fieldName, bind), lambda, bind));
221
236
  }
222
237
  const elementChildren = node.children.filter((c) => c.kind === "element");
223
238
  const textChildren = node.children.filter((c) => c.kind === "text");
@@ -227,7 +242,7 @@ export class TemplateCompiler {
227
242
  else if (textChildren.length > 0) {
228
243
  const parts = textChildren.flatMap((t) => t.parts).map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: this.resolve(p.expression, localScope) } : p));
229
244
  const interpolated = { kind: "InterpolatedStringLiteral", parts, line: at.line, col: at.col };
230
- statements.push(assign(member(self, "textContent", at), interpolated, at));
245
+ statements.push(assign(member(self, "TextContent", at), interpolated, at));
231
246
  }
232
247
  else {
233
248
  for (const child of elementChildren)
@@ -271,6 +286,19 @@ export class TemplateCompiler {
271
286
  return [...statements, this.appendChild(parentVar, elVar, node)];
272
287
  }
273
288
  appendChild(parentVar, childVar, at) {
274
- return exprStatement(call(member(ident(parentVar, at), "appendChild", at), [ident(childVar, at)], at), at);
289
+ return exprStatement(call(member(ident(parentVar, at), "AppendChild", at), [ident(childVar, at)], at), at);
290
+ }
291
+ // A known name (post-alias — "class" already reads "className" by here)
292
+ // becomes a direct assignment to VElement's own named field; anything
293
+ // else — href, src, alt, placeholder, ... — goes through VElement.SetAttr
294
+ // instead, under its original, real HTML attribute name (SetAttr's own
295
+ // name argument is exactly what gets passed to a real setAttribute call
296
+ // at patch time — see vdom.ks).
297
+ buildAttrAssignment(self, name, value, at) {
298
+ const fieldName = KNOWN_FIELD_NAMES[name];
299
+ if (fieldName) {
300
+ return assign(member(self, fieldName, at), value, at);
301
+ }
302
+ return exprStatement(call(member(self, "SetAttr", at), [stringLiteral(name, at), value], at), at);
275
303
  }
276
304
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",
7
7
  "author": "Joe Koppin <koppinjo@gmail.com>",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://dev.azure.com/koppinator/Koppindependence/_git/Kop"
10
+ "url": "https://dev.azure.com/koppinator/Koppindependence/_git/KopScript"
11
11
  },
12
12
  "homepage": "https://kopular.dev",
13
13
  "keywords": [