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 +30 -10
- package/README.md +54 -27
- package/dist/checker.js +87 -8
- package/dist/codegen.js +28 -4
- package/dist/template_compiler.js +37 -9
- package/package.json +2 -2
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)
|
|
242
|
-
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
|
|
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 —
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
(`
|
|
545
|
-
Kopular's `
|
|
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
|
|
542
|
-
|
|
543
|
-
button.
|
|
544
|
-
button.
|
|
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
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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.
|
|
605
|
-
| `(event)="stmt"` | `el.
|
|
606
|
-
| `[prop]="expr"` | `el.
|
|
607
|
-
| `class="..."` (static) | `el.
|
|
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
|
-
`
|
|
627
|
-
exactly the
|
|
628
|
-
|
|
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
|
|
680
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
1718
|
-
//
|
|
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
|
|
2007
|
-
// (derived from the callback
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
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
|
|
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) {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
const
|
|
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:
|
|
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:
|
|
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("
|
|
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(
|
|
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(
|
|
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(
|
|
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, "
|
|
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), "
|
|
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.
|
|
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/
|
|
10
|
+
"url": "https://dev.azure.com/koppinator/Koppindependence/_git/KopScript"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://kopular.dev",
|
|
13
13
|
"keywords": [
|