kopscript 0.17.0 → 0.19.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 +77 -4
- package/README.md +25 -2
- package/dist/checker.js +86 -11
- package/dist/parser.js +19 -2
- package/dist/printer.js +2 -1
- package/dist/template_compiler.js +32 -0
- package/dist/template_parser.js +11 -2
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -167,13 +167,51 @@ string Classify(string input) {
|
|
|
167
167
|
return match input {
|
|
168
168
|
"cat", "dog" => "animal", // comma = OR within one arm
|
|
169
169
|
r"^[0-9]+$" => "number", // regex pattern, r"..."
|
|
170
|
-
_ => "unknown" // wildcard REQUIRED
|
|
170
|
+
_ => "unknown" // wildcard REQUIRED for a string subject
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
`match` is an expression (has a value)
|
|
176
|
-
|
|
175
|
+
`match` is an expression (has a value) over one of two kinds of subject:
|
|
176
|
+
|
|
177
|
+
- **`string`** — literal/regex patterns only (not type patterns, not destructuring),
|
|
178
|
+
and always requires a trailing `_` arm, exactly as above.
|
|
179
|
+
- **an `enum`** — patterns name a member of that same enum (`EnumName.Member`, comma
|
|
180
|
+
for OR within one arm, same as a string match); no regex patterns (there's nothing
|
|
181
|
+
to run a regex against). **Real exhaustiveness**: covering every declared member
|
|
182
|
+
lets the trailing `_` be omitted entirely — there's nothing left for it to catch —
|
|
183
|
+
and a member missing from BOTH the arms and a trailing wildcard is a compile error
|
|
184
|
+
naming exactly which member(s) are missing:
|
|
185
|
+
|
|
186
|
+
```ks
|
|
187
|
+
enum OrderState { Pending, Shipped, Delivered, Cancelled }
|
|
188
|
+
|
|
189
|
+
string Describe(OrderState s) {
|
|
190
|
+
return match s {
|
|
191
|
+
OrderState.Pending => "placed",
|
|
192
|
+
OrderState.Shipped => "shipped",
|
|
193
|
+
OrderState.Delivered => "delivered",
|
|
194
|
+
OrderState.Cancelled => "cancelled"
|
|
195
|
+
// no `_` needed — every member is covered; add a 5th enum member later
|
|
196
|
+
// without updating this match, and this stops compiling.
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A partial enum match still works exactly like a string match — cover the cases you
|
|
202
|
+
care about and fall back with `_`:
|
|
203
|
+
|
|
204
|
+
```ks
|
|
205
|
+
string UrgencyOf(OrderState s) {
|
|
206
|
+
return match s {
|
|
207
|
+
OrderState.Cancelled => "none",
|
|
208
|
+
_ => "normal"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`match` still doesn't support type patterns or destructuring for either subject kind
|
|
214
|
+
— an enum match's patterns are exactly its own members, nothing more general.
|
|
177
215
|
|
|
178
216
|
## Statements
|
|
179
217
|
|
|
@@ -233,6 +271,18 @@ literal in the language, which do interpret `\n`/`\t`/`\\`/etc.
|
|
|
233
271
|
lambda's own first parameter cannot itself be a function type (a rare v1 parser
|
|
234
272
|
limitation, not a semantic one).
|
|
235
273
|
|
|
274
|
+
**An expression-bodied lambda may discard a non-`void` result where `() => void` is
|
|
275
|
+
expected** — the fire-and-forget shape a `setTimeout`/`setInterval` callback needs:
|
|
276
|
+
```ks
|
|
277
|
+
public async task DoWork() { ... }
|
|
278
|
+
SetTimeout(() => this.DoWork(), 300); // () => task discarded into () => void — fine
|
|
279
|
+
```
|
|
280
|
+
This is narrower than "anything is assignable to `void`": a `void`-returning
|
|
281
|
+
function's own `return expr;` is still a real error, checked separately — this only
|
|
282
|
+
relaxes a lambda's own trailing expression value when nothing will ever read it,
|
|
283
|
+
matching the same allowance TypeScript gives a `Promise`-returning function passed
|
|
284
|
+
where `() => void` is expected.
|
|
285
|
+
|
|
236
286
|
### Arrays
|
|
237
287
|
|
|
238
288
|
```ks
|
|
@@ -403,6 +453,16 @@ parameter, not a syntax feature to reach for. A constrained type parameter (`T :
|
|
|
403
453
|
IComparable`) works exactly like it does on a class — the inferred type must satisfy it,
|
|
404
454
|
checked after inference succeeds.
|
|
405
455
|
|
|
456
|
+
**Extern generic free functions**: an `extern` free function can carry its own type
|
|
457
|
+
parameter list too, re-describing a real generic free function from another package
|
|
458
|
+
(the same shape `extern class Name<T> { ... }` already gives a generic class):
|
|
459
|
+
```ks
|
|
460
|
+
extern (T) => string? CombineValidators2<T>((T) => string? v1, (T) => string? v2) from "kopular/forms";
|
|
461
|
+
(string) => string? combined = CombineValidators2(Required, TooLong);
|
|
462
|
+
```
|
|
463
|
+
Inference, constraints, and arity all work exactly the same as a real generic
|
|
464
|
+
function's — the extern boundary doesn't lose any of it.
|
|
465
|
+
|
|
406
466
|
**Does not exist (v1 scope cuts, each deliberate)**:
|
|
407
467
|
- **Generic methods.** A class method can't introduce its own new type parameter beyond
|
|
408
468
|
its enclosing class's (generic functions are free-function-only — see above).
|
|
@@ -492,6 +552,13 @@ constraint errors included) — and a real KopScript class can extend a generic
|
|
|
492
552
|
class` with a concrete or threaded-through type argument, same as extending a real generic
|
|
493
553
|
base.
|
|
494
554
|
|
|
555
|
+
An extern *free function* (the first form above) can carry its own type parameter list
|
|
556
|
+
too, the same place a real generic free function's goes — right after the name:
|
|
557
|
+
`extern (T) => string? CombineValidators2<T>((T) => string? v1, (T) => string? v2) from
|
|
558
|
+
"kopular/forms";`. Inference/constraints/arity all work identically to a real generic
|
|
559
|
+
function (see "Generics" above) — only an extern *value* declaration (the second form)
|
|
560
|
+
can never have one, since there's no call site for a type argument to attach to.
|
|
561
|
+
|
|
495
562
|
**Never write `async` on an extern function/method signature** — declare its return type
|
|
496
563
|
as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
|
|
497
564
|
`async` only means something for a *body* the checker validates (legalizing `await`
|
|
@@ -550,6 +617,12 @@ class Counter : Component {
|
|
|
550
617
|
`[prop]="expr"` (→ a plain field assignment for `id`/`className`/`value`, or
|
|
551
618
|
`VElement.SetAttr("prop", expr)` for anything else), static `attr="..."` (`class` aliases
|
|
552
619
|
to `className`).
|
|
620
|
+
- **Two-way binding**: `[(value)]="Field"` → `[value]="Field"` + an auto-generated
|
|
621
|
+
`(input)="Field = e.target.value"`. `value` only (`KS5017` otherwise — no other
|
|
622
|
+
`VElement` field has an equivalent "user just changed this" event); `Field` must resolve
|
|
623
|
+
to a bare name or `this.Field` (`KS5018` otherwise — anything else has nothing sensible
|
|
624
|
+
to assign back into). A hand-written `Render()` has no equivalent shorthand — it already
|
|
625
|
+
has direct field/handler access, so there's nothing to desugar.
|
|
553
626
|
- Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
|
|
554
627
|
`for..in`; the element type is explicit — no inference, same stance as Generics).
|
|
555
628
|
At most one structural directive per element.
|
|
@@ -558,7 +631,7 @@ class Counter : Component {
|
|
|
558
631
|
manual `Subscribe` needed for that field. State reached indirectly (through a method call,
|
|
559
632
|
or `this.SomeService.Count`) still needs a manual `Subscribe`, unchanged from before.
|
|
560
633
|
- Exactly one top-level element per template (no auto-wrap — hard error). No mixing text and
|
|
561
|
-
element children under one element. No
|
|
634
|
+
element children under one element. No pipes, no stacked directives.
|
|
562
635
|
- `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
|
|
563
636
|
- Layering note: `template from` is kopscript grammar, but what it desugars *to*
|
|
564
637
|
(`VElement.Create`/`.AppendChild`/`.TextContent`/`.SetAttr`/the named `On*` event fields)
|
package/README.md
CHANGED
|
@@ -330,6 +330,12 @@ if (c == Color.Green) { print("It's green"); }
|
|
|
330
330
|
|
|
331
331
|
Members are numbered from `0` in declaration order, compiling to a frozen JS object.
|
|
332
332
|
|
|
333
|
+
An enum is also a valid `match` subject (`match c { Color.Red => ..., Color.Green =>
|
|
334
|
+
..., Color.Blue => ..., _ => ... }`, patterns naming its own members) — covering
|
|
335
|
+
every member lets the trailing `_` be omitted, with a real compile error if one is
|
|
336
|
+
missing and there's no `_` to fall back on. See LLM.md's "`match` expression" section
|
|
337
|
+
for the full exhaustiveness rules.
|
|
338
|
+
|
|
333
339
|
### Modules
|
|
334
340
|
|
|
335
341
|
`using "./shapes";` brings every `public` top-level declaration from that file (resolved
|
|
@@ -432,6 +438,11 @@ extern class Box<T> {
|
|
|
432
438
|
Box<number> nb = new Box<number>(5);
|
|
433
439
|
```
|
|
434
440
|
|
|
441
|
+
An extern *free function* can be generic the same way — `extern (T) => string?
|
|
442
|
+
CombineValidators2<T>((T) => string? v1, (T) => string? v2) from "kopular/forms";`
|
|
443
|
+
re-describes a real generic free function from another package, with the same
|
|
444
|
+
inference/constraints a local one gets.
|
|
445
|
+
|
|
435
446
|
Now that KopScript has `async`/`await` and `task<T>` (see below), a Promise-based JS API is
|
|
436
447
|
describable too — `extern task<string> Fetch(...) from "..." as "fetch";` is legitimate,
|
|
437
448
|
and `await`ing it works exactly like awaiting any other KopScript task. What's still not cleanly
|
|
@@ -606,10 +617,22 @@ Supported bindings and directives:
|
|
|
606
617
|
| `{{ expr }}` (in text) | `el.TextContent = $"...{expr}...";` (an `InterpolatedStringLiteral`, same as `$"..."`) |
|
|
607
618
|
| `(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
619
|
| `[prop]="expr"` | `el.Prop = expr;` for `id`/`className`/`value` (`VElement`'s own named fields); `el.SetAttr("prop", expr);` for anything else |
|
|
620
|
+
| `[(value)]="Field"` | sugar for `[value]="Field"` + `(input)="Field = e.target.value"` — see below |
|
|
609
621
|
| `class="..."` (static) | `el.ClassName = "...";` (aliased, since `class` is a KopScript keyword) |
|
|
610
622
|
| `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
|
|
611
623
|
| `*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) |
|
|
612
624
|
|
|
625
|
+
**Two-way binding**: `[(value)]="Field"` (a template-only sugar — there's no equivalent
|
|
626
|
+
shorthand for a hand-written `Render()`, which already has direct field/handler access)
|
|
627
|
+
desugars to exactly the pair you'd otherwise write by hand: a `[value]` property binding
|
|
628
|
+
plus an auto-generated `(input)` handler assigning back. Restricted to `value`
|
|
629
|
+
specifically — the one `VElement` field a user can change through direct interaction
|
|
630
|
+
(typing, picking an option); `[(id)]`/`[(className)]` are compile errors (`KS5017`), since
|
|
631
|
+
neither has an equivalent "user just changed this" event. The bound expression must be a
|
|
632
|
+
simple field reference (a bare name or `this.Field`, resolving the same way any other
|
|
633
|
+
binding does) — `[(value)]="Field.Trim()"` is a compile error (`KS5018`), since there's
|
|
634
|
+
nothing sensible to assign back into.
|
|
635
|
+
|
|
613
636
|
**Auto-subscribe**: a `state<T>` field declared directly on the component and referenced
|
|
614
637
|
directly in its template (like `Count` above) gets its `Subscribe((v) => this.Update())`
|
|
615
638
|
wired up automatically — no manual `Subscribe` call needed in the constructor. This is a
|
|
@@ -620,8 +643,8 @@ templates existed.
|
|
|
620
643
|
|
|
621
644
|
v1 cuts, same discipline as generics and nullable types: exactly one top-level element per
|
|
622
645
|
template (no auto-wrapping — a clear error instead); no mixing text and element children
|
|
623
|
-
under one element (
|
|
624
|
-
|
|
646
|
+
under one element (`VElement` has no text-node concept, only `.TextContent`); no pipes, no
|
|
647
|
+
stacking two structural directives on one element.
|
|
625
648
|
|
|
626
649
|
**A deliberate layering note**: the `template from` syntax lives in kopscript's own
|
|
627
650
|
grammar (Kopular can't extend a language it doesn't own), but what it desugars *to* —
|
package/dist/checker.js
CHANGED
|
@@ -194,14 +194,21 @@ export class Checker {
|
|
|
194
194
|
this.checkClassBody(c);
|
|
195
195
|
}
|
|
196
196
|
registerExternFunction(decl) {
|
|
197
|
-
//
|
|
198
|
-
// generic
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
197
|
+
// Same withTypeParamsInScope treatment registerFunction gives a real
|
|
198
|
+
// generic function's own signature — an extern free function can be
|
|
199
|
+
// generic too (re-describing a real generic free function from another
|
|
200
|
+
// package, e.g. Kopular's own `CombineValidators2<T>`), resolved the
|
|
201
|
+
// same way so `T` inside `decl.params`/`decl.returnType` resolves
|
|
202
|
+
// correctly instead of erroring as an unknown type.
|
|
203
|
+
const { params, returnType } = this.withTypeParamsInScope(decl.typeParams, () => ({
|
|
203
204
|
params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
204
205
|
returnType: this.resolveType(decl.returnType, decl.line, decl.col),
|
|
206
|
+
}));
|
|
207
|
+
this.functions.set(decl.name, {
|
|
208
|
+
typeParams: decl.typeParams.map((p) => p.name),
|
|
209
|
+
typeParamConstraints: decl.typeParams.map((p) => p.constraint),
|
|
210
|
+
params,
|
|
211
|
+
returnType,
|
|
205
212
|
});
|
|
206
213
|
}
|
|
207
214
|
registerExternClass(decl) {
|
|
@@ -1644,6 +1651,19 @@ export class Checker {
|
|
|
1644
1651
|
return expectedReturnType;
|
|
1645
1652
|
}
|
|
1646
1653
|
const actual = this.checkExpressionExpecting(expr.body, expectedReturnType, scope, lambdaCtx);
|
|
1654
|
+
// An expression-bodied lambda whose expected type is `() => void` may
|
|
1655
|
+
// still have a body that naturally evaluates to something non-void
|
|
1656
|
+
// (`() => this.SubmitTask()` where SubmitTask is `async task`, the
|
|
1657
|
+
// fire-and-forget shape `setTimeout`/`setInterval` callbacks need) —
|
|
1658
|
+
// discarding that value is always safe, the same allowance TypeScript
|
|
1659
|
+
// gives a `Promise`-returning function passed where `() => void` is
|
|
1660
|
+
// expected. This is narrower than "anything is assignable to void": a
|
|
1661
|
+
// `void`-returning FUNCTION's own `return expr;` is still checked
|
|
1662
|
+
// separately and is still a real error — this only relaxes a lambda's
|
|
1663
|
+
// own trailing expression value when nothing will ever read it.
|
|
1664
|
+
if (expectedReturnType.kind === "void") {
|
|
1665
|
+
return T.VOID;
|
|
1666
|
+
}
|
|
1647
1667
|
if (expectedReturnType.kind !== "unknown" && !this.isAssignableType(actual, expectedReturnType)) {
|
|
1648
1668
|
this.diagnostics.error("KS4059", `Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col);
|
|
1649
1669
|
}
|
|
@@ -2176,30 +2196,69 @@ export class Checker {
|
|
|
2176
2196
|
}
|
|
2177
2197
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
2178
2198
|
}
|
|
2199
|
+
// `match` accepts two kinds of subject: `string` (the original, matching
|
|
2200
|
+
// literal/regex patterns — unchanged below) and, as of this check, an
|
|
2201
|
+
// `enum` (matching `EnumName.Member` patterns, with real exhaustiveness:
|
|
2202
|
+
// every member covered lets the trailing `_` be omitted entirely, since
|
|
2203
|
+
// there's nothing left for it to catch). Nothing else is valid — a
|
|
2204
|
+
// `match` over a class/number/etc. was never supported and still isn't.
|
|
2179
2205
|
checkMatch(expr, scope, ctx) {
|
|
2180
2206
|
const subjectType = this.checkExpression(expr.subject, scope, ctx);
|
|
2181
|
-
|
|
2207
|
+
const isEnumSubject = subjectType.kind === "enum";
|
|
2208
|
+
// Still routed through the shared expectType helper (one literal
|
|
2209
|
+
// KS4046 emission site for the whole checker — see diagnostics.test.ts's
|
|
2210
|
+
// "codes are unique across every emission site"), so the message loses
|
|
2211
|
+
// the "...or an enum" alternative when the subject is neither — an
|
|
2212
|
+
// acceptable trade against introducing a second call site for what's
|
|
2213
|
+
// still fundamentally "wrong type here."
|
|
2214
|
+
if (!isEnumSubject) {
|
|
2215
|
+
this.expectType(subjectType, T.STRING, expr.line, expr.col, "match subject");
|
|
2216
|
+
}
|
|
2217
|
+
const enumName = isEnumSubject ? subjectType.name : null;
|
|
2182
2218
|
if (expr.arms.length === 0) {
|
|
2183
2219
|
this.diagnostics.error("KS4086", `match expression must have at least one arm`, expr.line, expr.col);
|
|
2184
2220
|
return T.UNKNOWN;
|
|
2185
2221
|
}
|
|
2186
2222
|
const lastArm = expr.arms[expr.arms.length - 1];
|
|
2187
|
-
|
|
2223
|
+
const hasTrailingWildcard = lastArm.pattern.kind === "WildcardPattern";
|
|
2224
|
+
// A plain string match still always requires it (unchanged from
|
|
2225
|
+
// before enums were supported) — only an enum subject can skip it, and
|
|
2226
|
+
// only by covering every member explicitly instead (checked below,
|
|
2227
|
+
// once every arm's patterns have been walked).
|
|
2228
|
+
if (!hasTrailingWildcard && !isEnumSubject) {
|
|
2188
2229
|
this.diagnostics.error("KS4087", `match expression must end with a wildcard '_' arm`, lastArm.line, lastArm.col);
|
|
2189
2230
|
}
|
|
2231
|
+
const coveredEnumMembers = new Set();
|
|
2190
2232
|
let resultType = null;
|
|
2191
2233
|
for (const arm of expr.arms) {
|
|
2192
2234
|
if (arm.pattern.kind === "LiteralPattern") {
|
|
2193
2235
|
for (const value of arm.pattern.values) {
|
|
2194
|
-
if (
|
|
2195
|
-
this.
|
|
2236
|
+
if (isEnumSubject) {
|
|
2237
|
+
const valueType = this.checkExpression(value, scope, ctx);
|
|
2238
|
+
const isSameEnumMember = value.kind === "MemberExpr" && valueType.kind === "enum" && valueType.name === enumName;
|
|
2239
|
+
if (isSameEnumMember) {
|
|
2240
|
+
coveredEnumMembers.add(value.property);
|
|
2241
|
+
}
|
|
2242
|
+
else if (valueType.kind !== "unknown") {
|
|
2243
|
+
// valueType === "unknown" means checkExpression already
|
|
2244
|
+
// reported its own error (e.g. "Enum has no member X") —
|
|
2245
|
+
// don't pile a second, less specific one on top of it.
|
|
2246
|
+
this.reportInvalidMatchPattern(`match pattern must be a member of enum '${enumName}' (e.g. '${enumName}.SomeMember')`, value.line, value.col);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
else if (value.kind !== "StringLiteral") {
|
|
2250
|
+
this.reportInvalidMatchPattern(`match patterns must be string literals`, value.line, value.col);
|
|
2196
2251
|
}
|
|
2197
2252
|
else {
|
|
2198
2253
|
this.checkExpression(value, scope, ctx);
|
|
2199
2254
|
}
|
|
2200
2255
|
}
|
|
2201
2256
|
}
|
|
2202
|
-
|
|
2257
|
+
else if (arm.pattern.kind === "RegexPattern" && isEnumSubject) {
|
|
2258
|
+
this.reportInvalidMatchPattern(`a regex pattern is not valid for a match over enum '${enumName}' — enum patterns must name a member`, arm.line, arm.col);
|
|
2259
|
+
}
|
|
2260
|
+
// WildcardPattern (and RegexPattern against a string subject) need no
|
|
2261
|
+
// further checking here.
|
|
2203
2262
|
const armResultType = this.checkExpression(arm.result, scope, ctx);
|
|
2204
2263
|
if (resultType === null) {
|
|
2205
2264
|
resultType = armResultType;
|
|
@@ -2208,6 +2267,22 @@ export class Checker {
|
|
|
2208
2267
|
this.diagnostics.error("KS4089", `match arm result type '${T.typeToString(armResultType)}' does not match preceding arms' type '${T.typeToString(resultType)}'`, arm.line, arm.col);
|
|
2209
2268
|
}
|
|
2210
2269
|
}
|
|
2270
|
+
if (isEnumSubject && enumName && !hasTrailingWildcard) {
|
|
2271
|
+
const allMembers = [...(this.enums.get(enumName)?.members.keys() ?? [])];
|
|
2272
|
+
const missing = allMembers.filter((m) => !coveredEnumMembers.has(m));
|
|
2273
|
+
if (missing.length > 0) {
|
|
2274
|
+
this.diagnostics.error("KS4105", `match over enum '${enumName}' is not exhaustive — missing case(s) for: ${missing.join(", ")} (add a case for each, or a trailing '_' arm)`, expr.line, expr.col);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2211
2277
|
return resultType ?? T.UNKNOWN;
|
|
2212
2278
|
}
|
|
2279
|
+
// The one literal KS4088 emission site (three call sites in checkMatch
|
|
2280
|
+
// route through it — see diagnostics.test.ts's "codes are unique across
|
|
2281
|
+
// every emission site"): a match pattern that isn't valid for its
|
|
2282
|
+
// subject's kind (not a string literal against a string subject, not a
|
|
2283
|
+
// same-enum member reference against an enum subject, or a regex pattern
|
|
2284
|
+
// against an enum subject at all).
|
|
2285
|
+
reportInvalidMatchPattern(message, line, col) {
|
|
2286
|
+
this.diagnostics.error("KS4088", message, line, col);
|
|
2287
|
+
}
|
|
2213
2288
|
}
|
package/dist/parser.js
CHANGED
|
@@ -214,7 +214,7 @@ export class Parser {
|
|
|
214
214
|
this.diagnostics.error("KS2006", "'async' cannot modify a variable declaration", start.line, start.col);
|
|
215
215
|
}
|
|
216
216
|
if (typeParams.length > 0) {
|
|
217
|
-
this.
|
|
217
|
+
this.reportStrayTypeParamList(start.line, start.col);
|
|
218
218
|
}
|
|
219
219
|
this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
|
|
220
220
|
const init = this.parseExpression();
|
|
@@ -503,16 +503,33 @@ export class Parser {
|
|
|
503
503
|
}
|
|
504
504
|
const type = this.parseType();
|
|
505
505
|
const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
|
|
506
|
+
// `extern (T) => string? CombineValidators2<T>(...) from "...";` — same
|
|
507
|
+
// type-param-list-right-after-the-name shape a real generic free
|
|
508
|
+
// function uses (see parseDeclaration); harmless to try unconditionally
|
|
509
|
+
// here too, since parseTypeParamList consumes nothing when `<` isn't
|
|
510
|
+
// next.
|
|
511
|
+
const typeParams = this.parseTypeParamList();
|
|
506
512
|
if (this.check(TokenKind.LParen)) {
|
|
507
513
|
const params = this.parseParamList();
|
|
508
514
|
const { modulePath, jsName } = this.parseExternTail(name);
|
|
509
515
|
this.consume(TokenKind.Semicolon, "Expected ';' after extern function declaration");
|
|
510
|
-
return { kind: "ExternFunctionDecl", isExported, name, jsName, params, returnType: type, modulePath, line: start.line, col: start.col };
|
|
516
|
+
return { kind: "ExternFunctionDecl", isExported, name, typeParams, jsName, params, returnType: type, modulePath, line: start.line, col: start.col };
|
|
517
|
+
}
|
|
518
|
+
if (typeParams.length > 0) {
|
|
519
|
+
this.reportStrayTypeParamList(start.line, start.col);
|
|
511
520
|
}
|
|
512
521
|
const { modulePath, jsName } = this.parseExternTail(name);
|
|
513
522
|
this.consume(TokenKind.Semicolon, "Expected ';' after extern declaration");
|
|
514
523
|
return { kind: "ExternValueDecl", isExported, name, jsName, type, modulePath, line: start.line, col: start.col };
|
|
515
524
|
}
|
|
525
|
+
// The one literal KS2023 emission site (both parseDeclaration's plain
|
|
526
|
+
// VarDecl case and parseExternDecl's ExternValueDecl case route through
|
|
527
|
+
// it — see diagnostics.test.ts's "codes are unique across every emission
|
|
528
|
+
// site"): a type-parameter list appeared somewhere only a function
|
|
529
|
+
// declaration (real or extern) may have one.
|
|
530
|
+
reportStrayTypeParamList(line, col) {
|
|
531
|
+
this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", line, col);
|
|
532
|
+
}
|
|
516
533
|
// Optional `from "<path>"` (omit for an ambient global) and optional
|
|
517
534
|
// `as "<jsName>"` (omit when the JS-side name matches the KopScript-declared one).
|
|
518
535
|
parseExternTail(defaultJsName) {
|
package/dist/printer.js
CHANGED
|
@@ -276,7 +276,8 @@ export class Printer {
|
|
|
276
276
|
const prefix = decl.isExported ? "" : "private ";
|
|
277
277
|
const as = decl.jsName !== decl.name ? ` as "${decl.jsName}"` : "";
|
|
278
278
|
const from = decl.modulePath ? ` from "${decl.modulePath}"` : "";
|
|
279
|
-
|
|
279
|
+
const nameWithTypeParams = this.printTypeParamName(decl);
|
|
280
|
+
return `${pad}${prefix}extern ${this.printType(decl.returnType)} ${nameWithTypeParams}(${this.printParams(decl.params)})${from}${as};`;
|
|
280
281
|
}
|
|
281
282
|
printExternValue(decl, indent) {
|
|
282
283
|
const pad = indentStr(indent);
|
|
@@ -234,6 +234,8 @@ export class TemplateCompiler {
|
|
|
234
234
|
};
|
|
235
235
|
statements.push(assign(member(self, fieldName, bind), lambda, bind));
|
|
236
236
|
}
|
|
237
|
+
for (const bind of node.twoWayBindings)
|
|
238
|
+
statements.push(...this.buildTwoWayBinding(self, bind, localScope));
|
|
237
239
|
const elementChildren = node.children.filter((c) => c.kind === "element");
|
|
238
240
|
const textChildren = node.children.filter((c) => c.kind === "text");
|
|
239
241
|
if (textChildren.length > 0 && elementChildren.length > 0) {
|
|
@@ -301,4 +303,34 @@ export class TemplateCompiler {
|
|
|
301
303
|
}
|
|
302
304
|
return exprStatement(call(member(self, "SetAttr", at), [stringLiteral(name, at), value], at), at);
|
|
303
305
|
}
|
|
306
|
+
// `[(value)]="Field"` desugars to exactly the two-piece pattern you'd
|
|
307
|
+
// otherwise write by hand — `[value]="Field"` (a plain property binding)
|
|
308
|
+
// plus `(input)="Field = e.target.value"` (an auto-generated handler
|
|
309
|
+
// assigning back). Restricted to `value` specifically: it's the one
|
|
310
|
+
// VElement field a user can change through direct interaction (typing,
|
|
311
|
+
// picking an option) with no event of Kopular's own in between — `id`/
|
|
312
|
+
// `className` have no equivalent "user just changed this" event, so
|
|
313
|
+
// there's nothing for a two-way binding to mean for them.
|
|
314
|
+
buildTwoWayBinding(self, bind, localScope) {
|
|
315
|
+
if (bind.name !== "value") {
|
|
316
|
+
this.diagnostics.error("KS5017", `Unsupported two-way binding '[(${bind.name})]' — only [(value)] is supported, the one VElement field a user can change through direct interaction`, bind.line, bind.col);
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const target = this.resolve(bind.target, localScope);
|
|
320
|
+
if (target.kind !== "Identifier" && target.kind !== "MemberExpr") {
|
|
321
|
+
this.diagnostics.error("KS5018", `A two-way binding target must be a simple field reference (a bare name or 'this.Field'), not a complex expression`, bind.line, bind.col);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const propAssign = this.buildAttrAssignment(self, bind.name, target, bind);
|
|
325
|
+
const assignBack = assign(target, member(member(ident("e", bind), "target", bind), "value", bind), bind);
|
|
326
|
+
const lambda = {
|
|
327
|
+
kind: "LambdaExpr",
|
|
328
|
+
params: [{ name: "e", type: EVENT_TYPE }],
|
|
329
|
+
body: block([assignBack], bind),
|
|
330
|
+
line: bind.line,
|
|
331
|
+
col: bind.col,
|
|
332
|
+
};
|
|
333
|
+
const eventAssign = assign(member(self, "OnInput", bind), lambda, bind);
|
|
334
|
+
return [propAssign, eventAssign];
|
|
335
|
+
}
|
|
304
336
|
}
|
package/dist/template_parser.js
CHANGED
|
@@ -162,6 +162,7 @@ export class TemplateParser {
|
|
|
162
162
|
const staticAttrs = [];
|
|
163
163
|
const propBindings = [];
|
|
164
164
|
const eventBindings = [];
|
|
165
|
+
const twoWayBindings = [];
|
|
165
166
|
let ifCondition = null;
|
|
166
167
|
let forBinding = null;
|
|
167
168
|
while (this.check(TemplateTokenKind.AttrName)) {
|
|
@@ -171,7 +172,15 @@ export class TemplateParser {
|
|
|
171
172
|
if (!valueTok)
|
|
172
173
|
continue;
|
|
173
174
|
const name = nameTok.lexeme;
|
|
174
|
-
|
|
175
|
+
// Checked before the plain `[prop]` case below — `[(value)]` also
|
|
176
|
+
// starts with `[` and ends with `]`, so it would otherwise be
|
|
177
|
+
// mistaken for a property binding named literally "(value)".
|
|
178
|
+
if (name.startsWith("[(") && name.endsWith(")]")) {
|
|
179
|
+
const propName = name.slice(2, -2);
|
|
180
|
+
const target = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
181
|
+
twoWayBindings.push({ name: propName, target, line: nameTok.line, col: nameTok.col });
|
|
182
|
+
}
|
|
183
|
+
else if (name.startsWith("(") && name.endsWith(")")) {
|
|
175
184
|
const eventName = name.slice(1, -1);
|
|
176
185
|
const handler = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
|
|
177
186
|
eventBindings.push({ name: eventName, handler, line: nameTok.line, col: nameTok.col });
|
|
@@ -226,7 +235,7 @@ export class TemplateParser {
|
|
|
226
235
|
this.diagnostics.error("KS5012", `Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
|
-
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
238
|
+
return { kind: "element", tag, staticAttrs, propBindings, eventBindings, twoWayBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
|
|
230
239
|
}
|
|
231
240
|
consumeEquals(attrNameTok) {
|
|
232
241
|
this.consume(TemplateTokenKind.Equals, attrNameTok, `Expected '=' after attribute '${attrNameTok.lexeme}'`);
|
package/package.json
CHANGED