kopscript 0.16.0 → 0.18.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
@@ -540,11 +540,22 @@ class Counter : Component {
540
540
  - One `template` per class; a class with both `template` and a hand-written `Render()` is a
541
541
  compile error.
542
542
  - Desugars, between parsing and checking, into the exact same `MethodDecl` AST a
543
- hand-written `Render()` would produce — no runtime template engine, no virtual DOM.
544
- `{{ }}`/binding contents are real KopScript, parsed and type-checked normally.
545
- - Bindings: `{{ expr }}` (text interpolation, `InterpolatedStringLiteral`), `(event)="stmt"`
546
- (→ `addEventListener`), `[prop]="expr"` (→ plain assignment, checked like any member
547
- 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`).
553
+ - **Two-way binding**: `[(value)]="Field"` → `[value]="Field"` + an auto-generated
554
+ `(input)="Field = e.target.value"`. `value` only (`KS5017` otherwise — no other
555
+ `VElement` field has an equivalent "user just changed this" event); `Field` must resolve
556
+ to a bare name or `this.Field` (`KS5018` otherwise — anything else has nothing sensible
557
+ to assign back into). A hand-written `Render()` has no equivalent shorthand — it already
558
+ has direct field/handler access, so there's nothing to desugar.
548
559
  - Structural directives: `*if="expr"` (→ real `if`), `*for="Type varName of expr"` (→ real
549
560
  `for..in`; the element type is explicit — no inference, same stance as Generics).
550
561
  At most one structural directive per element.
@@ -553,12 +564,12 @@ class Counter : Component {
553
564
  manual `Subscribe` needed for that field. State reached indirectly (through a method call,
554
565
  or `this.SomeService.Count`) still needs a manual `Subscribe`, unchanged from before.
555
566
  - Exactly one top-level element per template (no auto-wrap — hard error). No mixing text and
556
- element children under one element. No two-way binding, no pipes, no stacked directives.
567
+ element children under one element. No pipes, no stacked directives.
557
568
  - `ks watch` tracks the referenced `.html` file as well as `.ks` dependencies.
558
569
  - Layering note: `template from` is kopscript grammar, but what it desugars *to*
559
- (`document.createElement`/`.appendChild`/`.textContent`/`.addEventListener`) assumes
560
- Kopular's `dom.ks` DOM surface specifically — a deliberate, documented coupling, not a
561
- generic pluggable target.
570
+ (`VElement.Create`/`.AppendChild`/`.TextContent`/`.SetAttr`/the named `On*` event fields)
571
+ assumes Kopular's `velement.ks` surface specifically — a deliberate, documented coupling,
572
+ not a generic pluggable target.
562
573
 
563
574
  ## Keywords (reserved, lowercase, exact match)
564
575
 
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,13 +603,25 @@ 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
+ | `[(value)]="Field"` | sugar for `[value]="Field"` + `(input)="Field = e.target.value"` see below |
610
+ | `class="..."` (static) | `el.ClassName = "...";` (aliased, since `class` is a KopScript keyword) |
608
611
  | `*if="expr"` | a real `if (expr) { ... }` around the element's creation |
609
612
  | `*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
613
 
614
+ **Two-way binding**: `[(value)]="Field"` (a template-only sugar — there's no equivalent
615
+ shorthand for a hand-written `Render()`, which already has direct field/handler access)
616
+ desugars to exactly the pair you'd otherwise write by hand: a `[value]` property binding
617
+ plus an auto-generated `(input)` handler assigning back. Restricted to `value`
618
+ specifically — the one `VElement` field a user can change through direct interaction
619
+ (typing, picking an option); `[(id)]`/`[(className)]` are compile errors (`KS5017`), since
620
+ neither has an equivalent "user just changed this" event. The bound expression must be a
621
+ simple field reference (a bare name or `this.Field`, resolving the same way any other
622
+ binding does) — `[(value)]="Field.Trim()"` is a compile error (`KS5018`), since there's
623
+ nothing sensible to assign back into.
624
+
611
625
  **Auto-subscribe**: a `state<T>` field declared directly on the component and referenced
612
626
  directly in its template (like `Count` above) gets its `Subscribe((v) => this.Update())`
613
627
  wired up automatically — no manual `Subscribe` call needed in the constructor. This is a
@@ -618,14 +632,15 @@ templates existed.
618
632
 
619
633
  v1 cuts, same discipline as generics and nullable types: exactly one top-level element per
620
634
  template (no auto-wrapping — a clear error instead); no mixing text and element children
621
- under one element (Kopular's DOM surface has no text-node type, only `.textContent`); no
622
- two-way binding, no pipes, no stacking two structural directives on one element.
635
+ under one element (`VElement` has no text-node concept, only `.TextContent`); no pipes, no
636
+ stacking two structural directives on one element.
623
637
 
624
638
  **A deliberate layering note**: the `template from` syntax lives in kopscript's own
625
639
  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,
640
+ `VElement.Create`, `.AppendChild`, `.TextContent`, `.SetAttr`, the named `On*` event
641
+ fields — assumes exactly the surface
642
+ [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
643
+ `velement.ks` declares. That's a real coupling from the compiler to one specific consumer,
629
644
  accepted deliberately rather than building a generic pluggable desugaring-target system
630
645
  for a hypothetical second framework that doesn't exist today. If one ever does, that's the
631
646
  point to generalize this.
@@ -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,8 +232,10 @@ 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
  }
237
+ for (const bind of node.twoWayBindings)
238
+ statements.push(...this.buildTwoWayBinding(self, bind, localScope));
222
239
  const elementChildren = node.children.filter((c) => c.kind === "element");
223
240
  const textChildren = node.children.filter((c) => c.kind === "text");
224
241
  if (textChildren.length > 0 && elementChildren.length > 0) {
@@ -227,7 +244,7 @@ export class TemplateCompiler {
227
244
  else if (textChildren.length > 0) {
228
245
  const parts = textChildren.flatMap((t) => t.parts).map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: this.resolve(p.expression, localScope) } : p));
229
246
  const interpolated = { kind: "InterpolatedStringLiteral", parts, line: at.line, col: at.col };
230
- statements.push(assign(member(self, "textContent", at), interpolated, at));
247
+ statements.push(assign(member(self, "TextContent", at), interpolated, at));
231
248
  }
232
249
  else {
233
250
  for (const child of elementChildren)
@@ -271,6 +288,49 @@ export class TemplateCompiler {
271
288
  return [...statements, this.appendChild(parentVar, elVar, node)];
272
289
  }
273
290
  appendChild(parentVar, childVar, at) {
274
- return exprStatement(call(member(ident(parentVar, at), "appendChild", at), [ident(childVar, at)], at), at);
291
+ return exprStatement(call(member(ident(parentVar, at), "AppendChild", at), [ident(childVar, at)], at), at);
292
+ }
293
+ // A known name (post-alias — "class" already reads "className" by here)
294
+ // becomes a direct assignment to VElement's own named field; anything
295
+ // else — href, src, alt, placeholder, ... — goes through VElement.SetAttr
296
+ // instead, under its original, real HTML attribute name (SetAttr's own
297
+ // name argument is exactly what gets passed to a real setAttribute call
298
+ // at patch time — see vdom.ks).
299
+ buildAttrAssignment(self, name, value, at) {
300
+ const fieldName = KNOWN_FIELD_NAMES[name];
301
+ if (fieldName) {
302
+ return assign(member(self, fieldName, at), value, at);
303
+ }
304
+ return exprStatement(call(member(self, "SetAttr", at), [stringLiteral(name, at), value], at), at);
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];
275
335
  }
276
336
  }
@@ -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
- if (name.startsWith("(") && name.endsWith(")")) {
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
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.16.0",
3
+ "version": "0.18.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": [