kopscript 0.18.0 → 0.19.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 +70 -3
- package/README.md +11 -0
- package/dist/checker.js +86 -11
- package/dist/codegen.js +21 -2
- package/dist/modules.js +12 -1
- package/dist/parser.js +19 -2
- package/dist/printer.js +2 -1
- 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`
|
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
|
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/codegen.js
CHANGED
|
@@ -98,8 +98,27 @@ export class CodeGenerator {
|
|
|
98
98
|
// `public` into scope without naming it explicitly. `sourceFileName`/
|
|
99
99
|
// `sourceText` are for the emitted source map only (both optional so every
|
|
100
100
|
// existing test call site keeps compiling unchanged).
|
|
101
|
-
generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = ""
|
|
102
|
-
|
|
101
|
+
generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = "",
|
|
102
|
+
// Interface names visible via a `using` this module doesn't declare
|
|
103
|
+
// itself — separate from `usingExports` above, which deliberately
|
|
104
|
+
// excludes interfaces (they're compile-time only and never produce a
|
|
105
|
+
// JS import). Without this, a class implementing an interface declared
|
|
106
|
+
// in a DIFFERENT file (e.g. `class Component : Flushable` where
|
|
107
|
+
// `Flushable` lives in vdom.ks) has that interface name missing from
|
|
108
|
+
// `interfaceNames` entirely, so genClass's own "only a non-interface
|
|
109
|
+
// base list entry becomes `extends`" check can't tell it apart from a
|
|
110
|
+
// real (missing) superclass — it gets emitted as `extends Flushable`,
|
|
111
|
+
// a real runtime reference to a name nothing ever imports, throwing
|
|
112
|
+
// `ReferenceError: Flushable is not defined` the moment the class is
|
|
113
|
+
// ever loaded. Found for real building Kopular's own event-batching
|
|
114
|
+
// fix — the checker already resolves a cross-module interface's base
|
|
115
|
+
// list entry correctly (it type-checks fine); only codegen's own,
|
|
116
|
+
// separate, same-file-only interfaceNames set didn't know about it.
|
|
117
|
+
importedInterfaceNames = new Set()) {
|
|
118
|
+
this.interfaceNames = new Set([
|
|
119
|
+
...program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name),
|
|
120
|
+
...importedInterfaceNames,
|
|
121
|
+
]);
|
|
103
122
|
this.outputLine = 1;
|
|
104
123
|
const outFileName = sourceFileName.replace(/\.ks$/, ".js");
|
|
105
124
|
this.sourceMap = new SourceMapBuilder(outFileName, sourceFileName, sourceText);
|
package/dist/modules.js
CHANGED
|
@@ -226,6 +226,13 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
226
226
|
for (const absPath of order) {
|
|
227
227
|
const mod = modules.get(absPath);
|
|
228
228
|
const usingExports = new Map();
|
|
229
|
+
// Separate from usingExports (interfaces are excluded there — see its
|
|
230
|
+
// own comment below) — this is what tells codegen's own interfaceNames
|
|
231
|
+
// set about an interface declared in a DIFFERENT file this module
|
|
232
|
+
// `using`s, so a class implementing one doesn't get miscompiled into a
|
|
233
|
+
// real `extends <InterfaceName>` runtime reference (see
|
|
234
|
+
// CodeGenerator.generate's own comment on importedInterfaceNames).
|
|
235
|
+
const importedInterfaceNames = new Set();
|
|
229
236
|
for (const u of mod.program.usings) {
|
|
230
237
|
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
231
238
|
const depExports = exportsByModule.get(depPath);
|
|
@@ -234,13 +241,17 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
234
241
|
// they'd make an invalid import specifier if listed here.
|
|
235
242
|
const importableTypeNames = [...depExports.namedTypes.entries()].filter(([, kind]) => kind !== "interface").map(([name]) => name);
|
|
236
243
|
usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
|
|
244
|
+
for (const [name, kind] of depExports.namedTypes) {
|
|
245
|
+
if (kind === "interface")
|
|
246
|
+
importedInterfaceNames.add(name);
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
// Just the basename, not a cwd-relative path — the .js.map file always
|
|
240
251
|
// lands right next to its .ks/.js siblings (ks build writes output in
|
|
241
252
|
// place), so "sources" has to resolve relative to *that* directory, not
|
|
242
253
|
// wherever the compiler happened to be invoked from.
|
|
243
|
-
const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source);
|
|
254
|
+
const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source, importedInterfaceNames);
|
|
244
255
|
outputs.set(absPath, code);
|
|
245
256
|
sourceMaps.set(absPath, map);
|
|
246
257
|
}
|
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);
|
package/package.json
CHANGED