kopscript 0.4.1 → 0.5.1

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
@@ -281,7 +281,8 @@ Box<string> sb = new Box<string>("hi");
281
281
  print(nb.Get()); // 5
282
282
  ```
283
283
 
284
- One unconstrained, invariant type parameter per class/interface the whole feature.
284
+ One unconstrained, invariant type parameter per class/interface (real *or* `extern` — see
285
+ the `extern` section below) — the whole feature.
285
286
  Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
286
287
  compile to the exact same plain `class Box`), so there's no runtime cost and no way to
287
288
  inspect `T` at runtime.
@@ -387,6 +388,12 @@ KopScript-declared name exactly. Extern class members use real JS member names v
387
388
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
388
389
  declarations — each stands alone.
389
390
 
391
+ `extern class` can carry `<T>` (`extern class Box<T> { constructor(T v); T Value { get; } }
392
+ from "some-package";`) — identical rules to a real generic class (see "Generics" above:
393
+ one unconstrained invariant parameter, erased, no base-list generics), so a generic type
394
+ from another package instantiates and type-checks exactly like a local one
395
+ (`Box<number>`, arity/invariance errors included).
396
+
390
397
  **Never write `async` on an extern function/method signature** — declare its return type
391
398
  as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
392
399
  `async` only means something for a *body* the checker validates (legalizing `await`
package/README.md CHANGED
@@ -347,6 +347,21 @@ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...
347
347
  proving `extern` works as a real cross-*package* boundary, not just for describing DOM
348
348
  globals within a single project.
349
349
 
350
+ An `extern class` can carry its own type parameter, `extern class Box<T> { ... }` —
351
+ exactly the same single-type-parameter rules as a real generic class (see "Generics"
352
+ above: invariant, unconstrained, erased, can't appear in a base list), so a generic type
353
+ from another package (e.g. Kopular's `FormField<T>`) can be described and instantiated
354
+ generically, not just per concrete type:
355
+
356
+ ```ks
357
+ extern class Box<T> {
358
+ constructor(T v);
359
+ T Value { get; }
360
+ } from "some-package";
361
+
362
+ Box<number> nb = new Box<number>(5);
363
+ ```
364
+
350
365
  Now that KopScript has `async`/`await` and `task<T>` (see below), a Promise-based JS API is
351
366
  describable too — `extern task<string> Fetch(...) from "..." as "fetch";` is legitimate,
352
367
  and `await`ing it works exactly like awaiting any other KopScript task. What's still not cleanly
package/dist/checker.js CHANGED
@@ -114,6 +114,9 @@ export class Checker {
114
114
  for (const i of interfaceDecls)
115
115
  if (i.typeParam)
116
116
  this.genericTypeParams.set(i.name, i.typeParam);
117
+ for (const c of externClassDecls)
118
+ if (c.typeParam)
119
+ this.genericTypeParams.set(c.name, c.typeParam);
117
120
  for (const e of enumDecls)
118
121
  this.registerEnum(e);
119
122
  for (const i of interfaceDecls)
@@ -169,28 +172,35 @@ export class Checker {
169
172
  });
170
173
  }
171
174
  registerExternClass(decl) {
172
- const fields = new Map();
173
- const staticFields = new Map();
174
- for (const p of decl.properties) {
175
- const info = { type: this.resolveType(p.type, decl.line, decl.col), visibility: "public", hasSetter: p.hasSetter };
176
- (p.isStatic ? staticFields : fields).set(p.name, info);
177
- }
178
- const methods = new Map();
179
- const staticMethods = new Map();
180
- for (const m of decl.methods) {
181
- const info = {
182
- params: m.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
183
- returnType: this.resolveType(m.returnType, decl.line, decl.col),
184
- visibility: "public",
185
- isVirtual: m.isVirtual,
186
- isOverride: false,
187
- };
188
- (m.isStatic ? staticMethods : methods).set(m.name, info);
189
- }
190
- const ownCtorParams = decl.hasConstructor ? decl.ctorParams.map((p) => this.resolveType(p.type, decl.line, decl.col)) : null;
175
+ // Same withTypeParamInScope treatment registerClass gives a real class's
176
+ // signature (fields/methods/ctor params) — an extern class's own `T`
177
+ // needs to resolve the same way while its declared members are being
178
+ // resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
179
+ const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
180
+ const fields = new Map();
181
+ const staticFields = new Map();
182
+ for (const p of decl.properties) {
183
+ const info = { type: this.resolveType(p.type, decl.line, decl.col), visibility: "public", hasSetter: p.hasSetter };
184
+ (p.isStatic ? staticFields : fields).set(p.name, info);
185
+ }
186
+ const methods = new Map();
187
+ const staticMethods = new Map();
188
+ for (const m of decl.methods) {
189
+ const info = {
190
+ params: m.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
191
+ returnType: this.resolveType(m.returnType, decl.line, decl.col),
192
+ visibility: "public",
193
+ isVirtual: m.isVirtual,
194
+ isOverride: false,
195
+ };
196
+ (m.isStatic ? staticMethods : methods).set(m.name, info);
197
+ }
198
+ const ownCtorParams = decl.hasConstructor ? decl.ctorParams.map((p) => this.resolveType(p.type, decl.line, decl.col)) : null;
199
+ return { fields, staticFields, methods, staticMethods, ownCtorParams };
200
+ });
191
201
  this.classes.set(decl.name, {
192
202
  name: decl.name,
193
- typeParam: null,
203
+ typeParam: decl.typeParam,
194
204
  superclass: null,
195
205
  interfaces: [],
196
206
  fields,
package/dist/codegen.js CHANGED
@@ -147,10 +147,10 @@ export class CodeGenerator {
147
147
  const init = stmt.init
148
148
  ? stmt.init.kind === "VarDecl"
149
149
  ? `${stmt.init.isConst ? "const" : "let"} ${stmt.init.name} = ${this.genExpr(stmt.init.init)}`
150
- : this.genExpr(stmt.init.expression)
150
+ : this.genExprAtStatementLevel(stmt.init.expression)
151
151
  : "";
152
152
  const cond = stmt.condition ? this.genExpr(stmt.condition) : "";
153
- const update = stmt.update ? this.genExpr(stmt.update) : "";
153
+ const update = stmt.update ? this.genExprAtStatementLevel(stmt.update) : "";
154
154
  return `${pad}for (${init}; ${cond}; ${update}) ${this.genBlock(stmt.body, indent).trimStart()}`;
155
155
  }
156
156
  case "ForInStatement":
@@ -162,7 +162,7 @@ export class CodeGenerator {
162
162
  case "ContinueStatement":
163
163
  return `${pad}continue;`;
164
164
  case "ExpressionStatement":
165
- return `${pad}${this.genExpr(stmt.expression)};`;
165
+ return `${pad}${this.genExprAtStatementLevel(stmt.expression)};`;
166
166
  case "TryStatement":
167
167
  return this.genTry(stmt, indent);
168
168
  case "ThrowStatement":
@@ -245,6 +245,22 @@ export class CodeGenerator {
245
245
  return `${pad}constructor(${params}) ${bodyStr}`;
246
246
  }
247
247
  // ---------- expressions ----------
248
+ // genExpr wraps an assignment in parens unconditionally (`(x = y)`), since
249
+ // it has to stay safely embeddable wherever an expression can appear —
250
+ // inside a binary expression, a call argument, anywhere precedence could
251
+ // matter. At true statement level (an expression-statement, or a for
252
+ // loop's own init/update clause) nothing surrounds it that parens could
253
+ // ever matter for, so they're pure noise there — `(this.Count = 0);`
254
+ // reads like defensive generated code, not something a person would
255
+ // write by hand. Used at exactly those three call sites, not inside
256
+ // genExpr itself, since every other embedding context still needs the
257
+ // defensive parens.
258
+ genExprAtStatementLevel(expr) {
259
+ if (expr.kind === "AssignExpr") {
260
+ return `${this.genExpr(expr.target)} = ${this.genExpr(expr.value)}`;
261
+ }
262
+ return this.genExpr(expr);
263
+ }
248
264
  genExpr(expr) {
249
265
  switch (expr.kind) {
250
266
  case "NumberLiteral":
package/dist/parser.js CHANGED
@@ -455,6 +455,7 @@ export class Parser {
455
455
  }
456
456
  parseExternClassBody(start, isExported) {
457
457
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
458
+ const typeParam = this.parseOptionalTypeParam();
458
459
  this.consume(TokenKind.LBrace, "Expected '{' before extern class body");
459
460
  let hasConstructor = false;
460
461
  let ctorParams = [];
@@ -519,7 +520,7 @@ export class Parser {
519
520
  this.consume(TokenKind.RBrace, "Expected '}' after extern class body");
520
521
  const { modulePath, jsName } = this.parseExternTail(name);
521
522
  this.consume(TokenKind.Semicolon, "Expected ';' after extern class declaration");
522
- return { kind: "ExternClassDecl", isExported, name, jsName, hasConstructor, ctorParams, properties, methods, modulePath, line: start.line, col: start.col };
523
+ return { kind: "ExternClassDecl", isExported, name, typeParam, jsName, hasConstructor, ctorParams, properties, methods, modulePath, line: start.line, col: start.col };
523
524
  }
524
525
  parseBlock() {
525
526
  const start = this.consume(TokenKind.LBrace, "Expected '{'");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
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",