kopscript 0.10.0 → 0.11.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 +9 -0
- package/README.md +38 -7
- package/dist/cli.js +51 -4
- package/dist/printer.js +377 -0
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -15,6 +15,8 @@ ks build <file.ks> # type-check + emit <file>.js and <file>.js.map next
|
|
|
15
15
|
ks run <file.ks> # build, then execute with node --enable-source-maps
|
|
16
16
|
ks watch <file.ks> # build, then rebuild on every change to any file in the graph
|
|
17
17
|
ks check <file.ks> # type-check only, no output files
|
|
18
|
+
ks fmt <file.ks> # rewrite to canonical formatting, in place
|
|
19
|
+
ks fmt <file.ks> --check # exit 1 if not already formatted; writes nothing either way
|
|
18
20
|
ks build|check <file.ks> --json # single JSON object on stdout instead of human text
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -33,6 +35,13 @@ Source maps are statement-level, not column-level (see `src/sourcemap.ts`, hand-
|
|
|
33
35
|
zero added dependencies) — a thrown/uncaught error resolves to the right `.ks` line, but a
|
|
34
36
|
specific sub-expression within one line isn't separately mapped.
|
|
35
37
|
|
|
38
|
+
`ks fmt` (`src/printer.ts`) is single-file (lex+parse only, no `using`-graph/type-checking):
|
|
39
|
+
2-space indent, K&R braces, no trailing commas, double quotes only. Two things it does NOT
|
|
40
|
+
preserve, deliberately: original class-member order (always reprints as fields → properties
|
|
41
|
+
→ constructor → methods, since `ClassDecl` buckets members into separate arrays — see
|
|
42
|
+
"Classes" below — so any original interleaving is already gone from the AST) and one-line
|
|
43
|
+
collapsed bodies (every body always prints as a full multi-line block). Comments survive it.
|
|
44
|
+
|
|
36
45
|
## File shape
|
|
37
46
|
|
|
38
47
|
```ks
|
package/README.md
CHANGED
|
@@ -58,6 +58,10 @@ LLM's context, as opposed to this README's narrative explanation.
|
|
|
58
58
|
every `.js` file (original `.ks` embedded, no separate file to ship), and `ks run` enables
|
|
59
59
|
it automatically — an uncaught exception names the real `.ks` file and line, not the
|
|
60
60
|
generated JS. Hand-rolled VLQ encoder, zero added dependencies. See "Source maps" below.
|
|
61
|
+
- **A real formatter**: `ks fmt` rewrites a file to one canonical style in place — no
|
|
62
|
+
bikeshedding, no config to agree on. Comments survive it. Verified idempotent and
|
|
63
|
+
meaning-preserving (byte-identical compiled output before/after) against every real `.ks`
|
|
64
|
+
file in this repo and Kopular's own source. See "Formatter" below.
|
|
61
65
|
- **A companion framework, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)**:
|
|
62
66
|
components, constructor-injected services via a composition root (no DI container), and
|
|
63
67
|
real-URL routing (no config DSL) — built entirely on the features above, in a separate
|
|
@@ -749,6 +753,8 @@ ks build <file.ks> # type-check and emit <file>.js (+ <file>.js.map) next to t
|
|
|
749
753
|
ks run <file.ks> # build, then execute the emitted JS with node --enable-source-maps
|
|
750
754
|
ks watch <file.ks> # build, then rebuild on every change to any file in the graph
|
|
751
755
|
ks check <file.ks> # type-check only — no output files written
|
|
756
|
+
ks fmt <file.ks> # rewrite the file to canonical formatting, in place
|
|
757
|
+
ks fmt <file.ks> --check # exit 1 if it isn't already formatted; write nothing either way
|
|
752
758
|
```
|
|
753
759
|
|
|
754
760
|
`build` and `check` both accept `--json`, which replaces all human-readable output with a
|
|
@@ -793,7 +799,31 @@ This is a deliberate v1 cut, the same spirit as generics/nullable types/template
|
|
|
793
799
|
useful debugging (correct stack-trace lines, working breakpoints) without a full rewrite of
|
|
794
800
|
codegen's string-concatenation architecture into a position-tracking writer.
|
|
795
801
|
|
|
796
|
-
|
|
802
|
+
**Formatter**: `ks fmt <file.ks>` rewrites a file to canonical formatting in place —
|
|
803
|
+
2-space indent, K&R braces, single space around operators, one blank line between
|
|
804
|
+
`using`s and the first declaration and between top-level declarations, one blank line
|
|
805
|
+
between class members, no trailing commas, double quotes only. `ks fmt <file.ks> --check`
|
|
806
|
+
reports (exit 1) whether a file isn't already formatted, without writing anything — for CI,
|
|
807
|
+
the same convention `gofmt -l`/`prettier --check` use. Single-file only: formatting needs
|
|
808
|
+
only lexing and parsing, not the full `using`-graph resolution or type-checking `build`/
|
|
809
|
+
`check`/`watch` do. Comments survive formatting (`src/tokens.ts`'s `CommentTrivia`, attached
|
|
810
|
+
to tokens by the lexer and looked up by `src/printer.ts` per node it prints) — two
|
|
811
|
+
deliberate v1 choices worth knowing about, not silent surprises:
|
|
812
|
+
- **Class members print in a fixed canonical order** (fields, then properties, then the
|
|
813
|
+
constructor, then methods), regardless of how the original source interleaved them —
|
|
814
|
+
`ClassDecl` buckets members into separate arrays (see `src/ast.ts`), so any original
|
|
815
|
+
interleaving between e.g. a field and a method is already gone by the time the AST
|
|
816
|
+
exists; there's nothing to preserve.
|
|
817
|
+
- **Every body always prints as a full multi-line block**, even a single-statement one a
|
|
818
|
+
human might have collapsed onto one line — no "when do I collapse this" judgment call for
|
|
819
|
+
the formatter to get subtly wrong.
|
|
820
|
+
|
|
821
|
+
Verified against every file in `examples/` and Kopular's own real `.ks` sources: formatting
|
|
822
|
+
twice is a no-op (idempotent), the reformatted file reparses with zero diagnostics, and —
|
|
823
|
+
the real proof formatting never changes what a program means — the *generated JS* for the
|
|
824
|
+
original and the reformatted source is byte-for-byte identical.
|
|
825
|
+
|
|
826
|
+
During development, use `npm run ks -- <build|run|watch|check|fmt> <file.ks>` (backed by
|
|
797
827
|
`tsx`), or run `npm run build` to compile the TypeScript compiler itself to `dist/` and
|
|
798
828
|
use `node dist/cli.js` directly.
|
|
799
829
|
|
|
@@ -831,12 +861,13 @@ functions/classes.
|
|
|
831
861
|
language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
|
|
832
862
|
and no way to build a `task` value by hand outside an `async` function body.
|
|
833
863
|
|
|
834
|
-
|
|
835
|
-
which an AST-based pretty-printer would otherwise silently delete on
|
|
836
|
-
|
|
837
|
-
`
|
|
838
|
-
|
|
839
|
-
|
|
864
|
+
`ks fmt` now exists (see "Formatter" above) — it was blocked on the lexer discarding every
|
|
865
|
+
comment entirely, which an AST-based pretty-printer would otherwise silently delete on
|
|
866
|
+
reformat, until `Token.leadingComments` (`src/tokens.ts`'s `CommentTrivia`) closed that gap.
|
|
867
|
+
`src/printer.ts` looks up each comment by the line number of the token it attached to
|
|
868
|
+
(not a `(line, col)` pair — several node kinds, e.g. `FieldDecl`, deliberately record their
|
|
869
|
+
position at their *type* token, after any `public`/`static`/etc. modifiers, which a
|
|
870
|
+
column-exact lookup would miss).
|
|
840
871
|
|
|
841
872
|
### A frontend framework: Kopular
|
|
842
873
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { writeFileSync, watch as fsWatch } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, watch as fsWatch } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { resolve, basename, join } from "node:path";
|
|
5
5
|
import { compileGraph } from "./modules.js";
|
|
6
|
+
import { Lexer } from "./lexer.js";
|
|
7
|
+
import { Parser } from "./parser.js";
|
|
8
|
+
import { DiagnosticBag } from "./diagnostics.js";
|
|
9
|
+
import { Printer, collectComments } from "./printer.js";
|
|
6
10
|
function outputPathFor(filePath) {
|
|
7
11
|
const name = basename(filePath, ".ks");
|
|
8
12
|
return join(resolve(filePath, ".."), `${name}.js`);
|
|
@@ -153,13 +157,48 @@ function watchCommand(filePath) {
|
|
|
153
157
|
console.log(`[watch] ${filePath} — watching for changes. Press Ctrl+C to stop.`);
|
|
154
158
|
rebuild();
|
|
155
159
|
}
|
|
156
|
-
|
|
160
|
+
// Rewrites `filePath` to canonical formatting, or (with `check: true`)
|
|
161
|
+
// reports whether it already is without touching it — for CI, matching
|
|
162
|
+
// `gofmt -l`/`prettier --check`'s convention. Single-file only, unlike
|
|
163
|
+
// `build`/`check`/`watch`: formatting only needs lex+parse (see
|
|
164
|
+
// printer.ts), no `using`-graph resolution or type-checking, so there's no
|
|
165
|
+
// `compileGraph` call here at all.
|
|
166
|
+
function fmtCommand(filePath, check) {
|
|
167
|
+
if (!existsSync(filePath)) {
|
|
168
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const source = readFileSync(filePath, "utf-8");
|
|
173
|
+
const diagnostics = new DiagnosticBag();
|
|
174
|
+
const tokens = new Lexer(source, diagnostics).tokenize();
|
|
175
|
+
const program = new Parser(tokens, diagnostics).parseProgram();
|
|
176
|
+
if (diagnostics.hasErrors) {
|
|
177
|
+
console.error(diagnostics.format(source, filePath));
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const formatted = new Printer(collectComments(tokens)).print(program);
|
|
182
|
+
if (check) {
|
|
183
|
+
if (formatted !== source) {
|
|
184
|
+
console.error(`${filePath} is not formatted`);
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (formatted !== source) {
|
|
190
|
+
writeFileSync(filePath, formatted, "utf-8");
|
|
191
|
+
console.log(`Formatted ${filePath}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const COMMANDS = ["build", "run", "watch", "check", "fmt"];
|
|
157
195
|
function main() {
|
|
158
196
|
const args = process.argv.slice(2);
|
|
159
197
|
const jsonMode = args.includes("--json");
|
|
160
|
-
const
|
|
198
|
+
const checkMode = args.includes("--check");
|
|
199
|
+
const [command, file] = args.filter((a) => a !== "--json" && a !== "--check");
|
|
161
200
|
if (!command || !file || !COMMANDS.includes(command)) {
|
|
162
|
-
console.error("Usage: ks <build|run|watch|check> <file.ks> [--json]");
|
|
201
|
+
console.error("Usage: ks <build|run|watch|check|fmt> <file.ks> [--json] [--check]");
|
|
163
202
|
process.exitCode = 1;
|
|
164
203
|
return;
|
|
165
204
|
}
|
|
@@ -168,10 +207,18 @@ function main() {
|
|
|
168
207
|
process.exitCode = 1;
|
|
169
208
|
return;
|
|
170
209
|
}
|
|
210
|
+
if (checkMode && command !== "fmt") {
|
|
211
|
+
console.error("ks: --check is only supported with 'fmt'");
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
171
215
|
const filePath = resolve(file);
|
|
172
216
|
if (command === "check") {
|
|
173
217
|
checkCommand(filePath, jsonMode);
|
|
174
218
|
}
|
|
219
|
+
else if (command === "fmt") {
|
|
220
|
+
fmtCommand(filePath, checkMode);
|
|
221
|
+
}
|
|
175
222
|
else if (command === "build") {
|
|
176
223
|
const { outPath } = build(filePath, jsonMode);
|
|
177
224
|
if (outPath && !jsonMode)
|
package/dist/printer.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Builds the line -> comments lookup the Printer consults at every node it's
|
|
2
|
+
// about to print. A separate pass over the token stream, not parser surgery.
|
|
3
|
+
// Keyed by line number *alone*, not the full (line, col) position: a `//`
|
|
4
|
+
// comment always consumes to end-of-line (lexer.ts's `skipTrivia`), so the
|
|
5
|
+
// token it attaches to is either the first thing on its own following line,
|
|
6
|
+
// or shares a line with real code — either way, that line number is unique
|
|
7
|
+
// within a file, and unambiguous as a lookup key. This matters because
|
|
8
|
+
// several AST node kinds (`FieldDecl`/`MethodDecl`/`PropertyDecl` chief among
|
|
9
|
+
// them) deliberately record `line`/`col` at their *type* token, not their
|
|
10
|
+
// first token — parser.ts captures `memberStart` only after consuming any
|
|
11
|
+
// `public`/`static`/`async`/`virtual` modifiers — so a comment attached to
|
|
12
|
+
// the modifier keyword (the token a leading comment actually lands on) would
|
|
13
|
+
// never match an exact (line, col) key, even though it's unambiguously that
|
|
14
|
+
// member's comment. A handful of node shapes carry no `line`/`col` at all
|
|
15
|
+
// (`Param`, `LambdaParam`, `CatchParam`, the bare `TypeNode` variants,
|
|
16
|
+
// `ExternPropertySig`/`ExternMethodSig`) — a comment immediately before one
|
|
17
|
+
// of those has nothing to attach to and is dropped; a real, documented v1
|
|
18
|
+
// loss, not a silent regression (every comment is dropped everywhere today).
|
|
19
|
+
export function collectComments(tokens) {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const t of tokens) {
|
|
22
|
+
if (t.leadingComments.length > 0)
|
|
23
|
+
map.set(t.line, t.leadingComments);
|
|
24
|
+
}
|
|
25
|
+
return map;
|
|
26
|
+
}
|
|
27
|
+
function indentStr(indent) {
|
|
28
|
+
return " ".repeat(indent);
|
|
29
|
+
}
|
|
30
|
+
// Re-escapes an already-unescaped string literal value back to source form
|
|
31
|
+
// (the lexer resolves `\n \t \r \\ \" \{ \}` at lex time — see
|
|
32
|
+
// lexer.ts's readStringChar). `escapeBraces` is only needed for an
|
|
33
|
+
// interpolated string's literal text chunks, where a bare `{`/`}` would
|
|
34
|
+
// otherwise be misread as starting a new interpolation hole on reparse;
|
|
35
|
+
// a plain `"..."` string has no such ambiguity, so leaves them alone.
|
|
36
|
+
function escapeStringValue(value, escapeBraces) {
|
|
37
|
+
let out = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
|
|
38
|
+
if (escapeBraces)
|
|
39
|
+
out = out.replace(/\{/g, "\\{").replace(/\}/g, "\\}");
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
const BIG_TOP_LEVEL_KINDS = new Set([
|
|
43
|
+
"ClassDecl",
|
|
44
|
+
"InterfaceDecl",
|
|
45
|
+
"EnumDecl",
|
|
46
|
+
"FunctionDecl",
|
|
47
|
+
"ExternFunctionDecl",
|
|
48
|
+
"ExternClassDecl",
|
|
49
|
+
"ExternValueDecl",
|
|
50
|
+
"RawStringDecl",
|
|
51
|
+
]);
|
|
52
|
+
// Prints an AST back to canonical KopScript source — the counterpart to
|
|
53
|
+
// codegen.ts's AST -> JS, structured the same way (one private method per
|
|
54
|
+
// node kind) so either file explains the other's shape. See README.md's
|
|
55
|
+
// "Formatter" section for the canonical style rules and the deliberate v1
|
|
56
|
+
// simplifications (always-multi-line bodies, imposed class-member order).
|
|
57
|
+
export class Printer {
|
|
58
|
+
constructor(comments) {
|
|
59
|
+
this.comments = comments;
|
|
60
|
+
}
|
|
61
|
+
print(program) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
for (const u of program.usings) {
|
|
64
|
+
this.emit(parts, u, `using "${u.path}";`, 0);
|
|
65
|
+
}
|
|
66
|
+
const usingsToBody = program.usings.length > 0 && program.statements.length > 0;
|
|
67
|
+
let prevWasBig = false;
|
|
68
|
+
program.statements.forEach((stmt, i) => {
|
|
69
|
+
const isBig = BIG_TOP_LEVEL_KINDS.has(stmt.kind);
|
|
70
|
+
const blankBefore = i === 0 ? usingsToBody : isBig || prevWasBig;
|
|
71
|
+
this.emit(parts, stmt, this.printStatement(stmt, 0), 0, blankBefore);
|
|
72
|
+
prevWasBig = isBig;
|
|
73
|
+
});
|
|
74
|
+
return parts.join("\n") + "\n";
|
|
75
|
+
}
|
|
76
|
+
// Looks up any comments recorded at `node`'s own position and pushes them
|
|
77
|
+
// (plus `text`) onto `parts` in the right shape: a comment tagged
|
|
78
|
+
// `sameLineAsPreviousToken` appends to the *previous* entry already in
|
|
79
|
+
// `parts` (a real trailing comment, e.g. `x = 5; // note`) instead of
|
|
80
|
+
// starting a new line; every other comment becomes its own leading line
|
|
81
|
+
// immediately before `text`, indented to match it. `blankLineBefore`
|
|
82
|
+
// (for the blank line between top-level declarations / class members)
|
|
83
|
+
// has to be inserted *after* a trailing comment attaches to the true
|
|
84
|
+
// previous line but *before* any leading comments/text — inserting it
|
|
85
|
+
// any earlier would make a trailing comment attach to the blank
|
|
86
|
+
// separator itself instead of the line it actually trails.
|
|
87
|
+
emit(parts, node, text, indent, blankLineBefore = false) {
|
|
88
|
+
const found = this.comments.get(node.line) ?? [];
|
|
89
|
+
let rest = found;
|
|
90
|
+
if (rest.length > 0 && rest[0].sameLineAsPreviousToken && parts.length > 0) {
|
|
91
|
+
parts[parts.length - 1] += ` //${rest[0].text}`;
|
|
92
|
+
rest = rest.slice(1);
|
|
93
|
+
}
|
|
94
|
+
if (blankLineBefore && parts.length > 0)
|
|
95
|
+
parts.push("");
|
|
96
|
+
const pad = indentStr(indent);
|
|
97
|
+
for (const c of rest)
|
|
98
|
+
parts.push(`${pad}//${c.text}`);
|
|
99
|
+
parts.push(text);
|
|
100
|
+
}
|
|
101
|
+
// ---------- types ----------
|
|
102
|
+
printType(type) {
|
|
103
|
+
switch (type.kind) {
|
|
104
|
+
case "NamedType":
|
|
105
|
+
return type.typeArgs ? `${type.name}<${type.typeArgs.map((t) => this.printType(t)).join(", ")}>` : type.name;
|
|
106
|
+
case "ArrayType":
|
|
107
|
+
return `${this.printType(type.element)}[]`;
|
|
108
|
+
case "FunctionType":
|
|
109
|
+
return `(${type.params.map((p) => this.printType(p)).join(", ")}) => ${this.printType(type.returnType)}`;
|
|
110
|
+
case "TaskType":
|
|
111
|
+
return type.resultType.kind === "NamedType" && type.resultType.name === "void" ? "task" : `task<${this.printType(type.resultType)}>`;
|
|
112
|
+
case "StateType":
|
|
113
|
+
return `state<${this.printType(type.valueType)}>`;
|
|
114
|
+
case "NullableType":
|
|
115
|
+
return `${this.printType(type.inner)}?`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
printParams(params) {
|
|
119
|
+
return params.map((p) => `${this.printType(p.type)} ${p.name}`).join(", ");
|
|
120
|
+
}
|
|
121
|
+
// ---------- top-level declarations ----------
|
|
122
|
+
printStatement(stmt, indent) {
|
|
123
|
+
const pad = indentStr(indent);
|
|
124
|
+
switch (stmt.kind) {
|
|
125
|
+
case "VarDecl":
|
|
126
|
+
return `${pad}${stmt.isConst ? "const " : ""}${this.printType(stmt.type)} ${stmt.name} = ${this.printExpr(stmt.init, indent)};`;
|
|
127
|
+
case "FunctionDecl":
|
|
128
|
+
return this.printFunction(stmt, indent);
|
|
129
|
+
case "ClassDecl":
|
|
130
|
+
return this.printClass(stmt, indent);
|
|
131
|
+
case "InterfaceDecl":
|
|
132
|
+
return this.printInterface(stmt, indent);
|
|
133
|
+
case "EnumDecl":
|
|
134
|
+
return `${pad}${stmt.isExported ? "" : "private "}enum ${stmt.name} { ${stmt.members.join(", ")} }`;
|
|
135
|
+
case "ExternFunctionDecl":
|
|
136
|
+
return this.printExternFunction(stmt, indent);
|
|
137
|
+
case "ExternClassDecl":
|
|
138
|
+
return this.printExternClass(stmt, indent);
|
|
139
|
+
case "ExternValueDecl":
|
|
140
|
+
return this.printExternValue(stmt, indent);
|
|
141
|
+
case "RawStringDecl":
|
|
142
|
+
return `${pad}${stmt.isExported ? "" : "private "}raw string ${stmt.name} from "${stmt.path}";`;
|
|
143
|
+
case "Block":
|
|
144
|
+
return this.printBlock(stmt, indent);
|
|
145
|
+
case "IfStatement":
|
|
146
|
+
return this.printIf(stmt, indent);
|
|
147
|
+
case "WhileStatement":
|
|
148
|
+
return `${pad}while (${this.printExpr(stmt.condition, indent)}) ${this.printBlock(stmt.body, indent).trimStart()}`;
|
|
149
|
+
case "ForStatement": {
|
|
150
|
+
const init = stmt.init ? (stmt.init.kind === "VarDecl" ? `${stmt.init.isConst ? "const " : ""}${this.printType(stmt.init.type)} ${stmt.init.name} = ${this.printExpr(stmt.init.init, indent)}` : this.printExpr(stmt.init.expression, indent)) : "";
|
|
151
|
+
const cond = stmt.condition ? this.printExpr(stmt.condition, indent) : "";
|
|
152
|
+
const update = stmt.update ? this.printExpr(stmt.update, indent) : "";
|
|
153
|
+
return `${pad}for (${init}; ${cond}; ${update}) ${this.printBlock(stmt.body, indent).trimStart()}`;
|
|
154
|
+
}
|
|
155
|
+
case "ForInStatement":
|
|
156
|
+
return `${pad}foreach (${this.printType(stmt.varType)} ${stmt.varName} in ${this.printExpr(stmt.iterable, indent)}) ${this.printBlock(stmt.body, indent).trimStart()}`;
|
|
157
|
+
case "ReturnStatement":
|
|
158
|
+
return stmt.value ? `${pad}return ${this.printExpr(stmt.value, indent)};` : `${pad}return;`;
|
|
159
|
+
case "BreakStatement":
|
|
160
|
+
return `${pad}break;`;
|
|
161
|
+
case "ContinueStatement":
|
|
162
|
+
return `${pad}continue;`;
|
|
163
|
+
case "ExpressionStatement":
|
|
164
|
+
return `${pad}${this.printExpr(stmt.expression, indent)};`;
|
|
165
|
+
case "TryStatement":
|
|
166
|
+
return this.printTry(stmt, indent);
|
|
167
|
+
case "ThrowStatement":
|
|
168
|
+
return `${pad}throw ${this.printExpr(stmt.expression, indent)};`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
printBlock(block, indent) {
|
|
172
|
+
const pad = indentStr(indent);
|
|
173
|
+
if (block.statements.length === 0)
|
|
174
|
+
return `{\n${pad}}`;
|
|
175
|
+
const parts = [];
|
|
176
|
+
for (const s of block.statements) {
|
|
177
|
+
this.emit(parts, s, this.printStatement(s, indent + 1), indent + 1);
|
|
178
|
+
}
|
|
179
|
+
return `{\n${parts.join("\n")}\n${pad}}`;
|
|
180
|
+
}
|
|
181
|
+
printIf(stmt, indent) {
|
|
182
|
+
const pad = indentStr(indent);
|
|
183
|
+
let code = `${pad}if (${this.printExpr(stmt.condition, indent)}) ${this.printBlock(stmt.thenBranch, indent).trimStart()}`;
|
|
184
|
+
if (stmt.elseBranch) {
|
|
185
|
+
code += stmt.elseBranch.kind === "IfStatement" ? ` else ${this.printIf(stmt.elseBranch, indent).trimStart()}` : ` else ${this.printBlock(stmt.elseBranch, indent).trimStart()}`;
|
|
186
|
+
}
|
|
187
|
+
return code;
|
|
188
|
+
}
|
|
189
|
+
printTry(stmt, indent) {
|
|
190
|
+
let code = `${indentStr(indent)}try ${this.printBlock(stmt.tryBlock, indent).trimStart()}`;
|
|
191
|
+
if (stmt.catchBlock && stmt.catchParam) {
|
|
192
|
+
code += ` catch (${this.printType(stmt.catchParam.type)} ${stmt.catchParam.name}) ${this.printBlock(stmt.catchBlock, indent).trimStart()}`;
|
|
193
|
+
}
|
|
194
|
+
if (stmt.finallyBlock) {
|
|
195
|
+
code += ` finally ${this.printBlock(stmt.finallyBlock, indent).trimStart()}`;
|
|
196
|
+
}
|
|
197
|
+
return code;
|
|
198
|
+
}
|
|
199
|
+
printFunction(decl, indent) {
|
|
200
|
+
const pad = indentStr(indent);
|
|
201
|
+
const prefix = `${decl.isExported ? "" : "private "}${decl.isAsync ? "async " : ""}`;
|
|
202
|
+
return `${pad}${prefix}${this.printType(decl.returnType)} ${decl.name}(${this.printParams(decl.params)}) ${this.printBlock(decl.body, indent).trimStart()}`;
|
|
203
|
+
}
|
|
204
|
+
// Canonical member order: fields, then properties, then the constructor,
|
|
205
|
+
// then methods — regardless of how the original source interleaved them.
|
|
206
|
+
// ClassDecl buckets members into four separate arrays (see ast.ts), so
|
|
207
|
+
// any original interleaving between e.g. a field and a method is already
|
|
208
|
+
// gone by the time the AST exists; this imposes one fixed, consistent
|
|
209
|
+
// order rather than guessing at how to restore it. A real, visible
|
|
210
|
+
// behavior on first-format of a file that interleaved members — not
|
|
211
|
+
// hidden, documented in README.md's "Formatter" section.
|
|
212
|
+
printClass(decl, indent) {
|
|
213
|
+
const pad = indentStr(indent);
|
|
214
|
+
const memberPad = indentStr(indent + 1);
|
|
215
|
+
const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
|
|
216
|
+
const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `class ${nameWithTypeParam} {`;
|
|
217
|
+
const prefix = decl.isExported ? "" : "private ";
|
|
218
|
+
const memberParts = [];
|
|
219
|
+
const pushMember = (node, text) => {
|
|
220
|
+
this.emit(memberParts, node, text, indent + 1, memberParts.length > 0);
|
|
221
|
+
};
|
|
222
|
+
for (const field of decl.fields) {
|
|
223
|
+
const modifiers = `${field.visibility}${field.isStatic ? " static" : ""}`;
|
|
224
|
+
const init = field.initializer ? ` = ${this.printExpr(field.initializer, indent + 1)}` : "";
|
|
225
|
+
pushMember(field, `${memberPad}${modifiers} ${this.printType(field.type)} ${field.name}${init};`);
|
|
226
|
+
}
|
|
227
|
+
for (const prop of decl.properties) {
|
|
228
|
+
const accessors = prop.hasSetter ? "get; set;" : "get;";
|
|
229
|
+
pushMember(prop, `${memberPad}${prop.visibility} ${this.printType(prop.type)} ${prop.name} { ${accessors} }`);
|
|
230
|
+
}
|
|
231
|
+
if (decl.constructor) {
|
|
232
|
+
pushMember(decl.constructor, this.printConstructor(decl.constructor, indent + 1));
|
|
233
|
+
}
|
|
234
|
+
for (const method of decl.methods) {
|
|
235
|
+
const modifiers = [method.visibility, method.isStatic ? "static" : null, method.isAsync ? "async" : null, method.isVirtual ? "virtual" : null, method.isOverride ? "override" : null].filter((m) => m !== null).join(" ");
|
|
236
|
+
pushMember(method, `${memberPad}${modifiers} ${this.printType(method.returnType)} ${method.name}(${this.printParams(method.params)}) ${this.printBlock(method.body, indent + 1).trimStart()}`);
|
|
237
|
+
}
|
|
238
|
+
if (decl.template) {
|
|
239
|
+
pushMember(decl.template, `${memberPad}template from "${decl.template.path}";`);
|
|
240
|
+
}
|
|
241
|
+
if (memberParts.length === 0)
|
|
242
|
+
return `${pad}${prefix}${header}\n${pad}}`;
|
|
243
|
+
return `${pad}${prefix}${header}\n${memberParts.join("\n")}\n${pad}}`;
|
|
244
|
+
}
|
|
245
|
+
printConstructor(ctor, indent) {
|
|
246
|
+
const pad = indentStr(indent);
|
|
247
|
+
const base = ctor.baseArgs ? ` : base(${ctor.baseArgs.map((a) => this.printExpr(a, indent)).join(", ")})` : "";
|
|
248
|
+
return `${pad}constructor(${this.printParams(ctor.params)})${base} ${this.printBlock(ctor.body, indent).trimStart()}`;
|
|
249
|
+
}
|
|
250
|
+
printInterface(decl, indent) {
|
|
251
|
+
const pad = indentStr(indent);
|
|
252
|
+
const memberPad = indentStr(indent + 1);
|
|
253
|
+
const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
|
|
254
|
+
const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `interface ${nameWithTypeParam} {`;
|
|
255
|
+
const prefix = decl.isExported ? "" : "private ";
|
|
256
|
+
if (decl.methods.length === 0)
|
|
257
|
+
return `${pad}${prefix}${header}\n${pad}}`;
|
|
258
|
+
const memberParts = [];
|
|
259
|
+
for (const m of decl.methods) {
|
|
260
|
+
this.emit(memberParts, m, `${memberPad}${this.printType(m.returnType)} ${m.name}(${this.printParams(m.params)});`, indent + 1);
|
|
261
|
+
}
|
|
262
|
+
return `${pad}${prefix}${header}\n${memberParts.join("\n")}\n${pad}}`;
|
|
263
|
+
}
|
|
264
|
+
printExternFunction(decl, indent) {
|
|
265
|
+
const pad = indentStr(indent);
|
|
266
|
+
const prefix = decl.isExported ? "" : "private ";
|
|
267
|
+
const as = decl.jsName !== decl.name ? ` as "${decl.jsName}"` : "";
|
|
268
|
+
const from = decl.modulePath ? ` from "${decl.modulePath}"` : "";
|
|
269
|
+
return `${pad}${prefix}extern ${this.printType(decl.returnType)} ${decl.name}(${this.printParams(decl.params)})${from}${as};`;
|
|
270
|
+
}
|
|
271
|
+
printExternValue(decl, indent) {
|
|
272
|
+
const pad = indentStr(indent);
|
|
273
|
+
const prefix = decl.isExported ? "" : "private ";
|
|
274
|
+
const as = decl.jsName !== decl.name ? ` as "${decl.jsName}"` : "";
|
|
275
|
+
const from = decl.modulePath ? ` from "${decl.modulePath}"` : "";
|
|
276
|
+
return `${pad}${prefix}extern ${this.printType(decl.type)} ${decl.name}${from}${as};`;
|
|
277
|
+
}
|
|
278
|
+
printExternClass(decl, indent) {
|
|
279
|
+
const pad = indentStr(indent);
|
|
280
|
+
const memberPad = indentStr(indent + 1);
|
|
281
|
+
const prefix = decl.isExported ? "" : "private ";
|
|
282
|
+
const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
|
|
283
|
+
const lines = [];
|
|
284
|
+
if (decl.hasConstructor)
|
|
285
|
+
lines.push(`${memberPad}constructor(${this.printParams(decl.ctorParams)});`);
|
|
286
|
+
for (const p of decl.properties) {
|
|
287
|
+
const accessors = p.hasSetter ? "get; set;" : "get;";
|
|
288
|
+
lines.push(`${memberPad}${p.isStatic ? "static " : ""}${this.printType(p.type)} ${p.name} { ${accessors} }`);
|
|
289
|
+
}
|
|
290
|
+
for (const m of decl.methods) {
|
|
291
|
+
const modifiers = `${m.isStatic ? "static " : ""}${m.isVirtual ? "virtual " : ""}`;
|
|
292
|
+
lines.push(`${memberPad}${modifiers}${this.printType(m.returnType)} ${m.name}(${this.printParams(m.params)});`);
|
|
293
|
+
}
|
|
294
|
+
const from = decl.modulePath ? ` from "${decl.modulePath}"` : "";
|
|
295
|
+
if (lines.length === 0)
|
|
296
|
+
return `${pad}${prefix}extern class ${nameWithTypeParam} {\n${pad}}${from};`;
|
|
297
|
+
return `${pad}${prefix}extern class ${nameWithTypeParam} {\n${lines.join("\n")}\n${pad}}${from};`;
|
|
298
|
+
}
|
|
299
|
+
// ---------- expressions ----------
|
|
300
|
+
// `indent` only matters to the two expression kinds that can themselves
|
|
301
|
+
// introduce a real newline (a `match` expression, a lambda with a block
|
|
302
|
+
// body) — every other case ignores it. Threaded through generally rather
|
|
303
|
+
// than special-cased at each call site, so a `match`/block-lambda buried
|
|
304
|
+
// inside a call argument several levels deep still indents correctly,
|
|
305
|
+
// not just one at the top of a statement.
|
|
306
|
+
printExpr(expr, indent = 0) {
|
|
307
|
+
switch (expr.kind) {
|
|
308
|
+
case "NumberLiteral":
|
|
309
|
+
return String(expr.value);
|
|
310
|
+
case "StringLiteral":
|
|
311
|
+
return `"${escapeStringValue(expr.value, false)}"`;
|
|
312
|
+
case "BoolLiteral":
|
|
313
|
+
return String(expr.value);
|
|
314
|
+
case "NullLiteral":
|
|
315
|
+
return "null";
|
|
316
|
+
case "InterpolatedStringLiteral":
|
|
317
|
+
return this.printInterpolatedString(expr, indent);
|
|
318
|
+
case "ArrayLiteral":
|
|
319
|
+
return `[${expr.elements.map((e) => this.printExpr(e, indent)).join(", ")}]`;
|
|
320
|
+
case "Identifier":
|
|
321
|
+
return expr.name;
|
|
322
|
+
case "ThisExpr":
|
|
323
|
+
return "this";
|
|
324
|
+
case "UnaryExpr":
|
|
325
|
+
return `${expr.op}${this.printExpr(expr.operand, indent)}`;
|
|
326
|
+
case "BinaryExpr":
|
|
327
|
+
return `${this.printExpr(expr.left, indent)} ${expr.op} ${this.printExpr(expr.right, indent)}`;
|
|
328
|
+
case "LogicalExpr":
|
|
329
|
+
return `${this.printExpr(expr.left, indent)} ${expr.op} ${this.printExpr(expr.right, indent)}`;
|
|
330
|
+
case "AssignExpr":
|
|
331
|
+
return `${this.printExpr(expr.target, indent)} = ${this.printExpr(expr.value, indent)}`;
|
|
332
|
+
case "CallExpr":
|
|
333
|
+
return `${this.printExpr(expr.callee, indent)}(${expr.args.map((a) => this.printExpr(a, indent)).join(", ")})`;
|
|
334
|
+
case "NewExpr": {
|
|
335
|
+
const typeArgs = expr.typeArgs ? `<${expr.typeArgs.map((t) => this.printType(t)).join(", ")}>` : "";
|
|
336
|
+
return `new ${expr.className}${typeArgs}(${expr.args.map((a) => this.printExpr(a, indent)).join(", ")})`;
|
|
337
|
+
}
|
|
338
|
+
case "MemberExpr":
|
|
339
|
+
return `${this.printExpr(expr.object, indent)}.${expr.property}`;
|
|
340
|
+
case "IndexExpr":
|
|
341
|
+
return `${this.printExpr(expr.object, indent)}[${this.printExpr(expr.index, indent)}]`;
|
|
342
|
+
case "MatchExpr":
|
|
343
|
+
return this.printMatch(expr, indent);
|
|
344
|
+
case "LambdaExpr":
|
|
345
|
+
return this.printLambda(expr, indent);
|
|
346
|
+
case "AwaitExpr":
|
|
347
|
+
return `await ${this.printExpr(expr.operand, indent)}`;
|
|
348
|
+
case "StateExpr":
|
|
349
|
+
return `state(${this.printExpr(expr.initializer, indent)})`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
printLambda(expr, indent) {
|
|
353
|
+
const params = expr.params.map((p) => `${this.printType(p.type)} ${p.name}`).join(", ");
|
|
354
|
+
if (expr.body.kind === "Block")
|
|
355
|
+
return `(${params}) => ${this.printBlock(expr.body, indent).trimStart()}`;
|
|
356
|
+
return `(${params}) => ${this.printExpr(expr.body, indent)}`;
|
|
357
|
+
}
|
|
358
|
+
printInterpolatedString(expr, indent) {
|
|
359
|
+
const body = expr.parts.map((part) => (part.kind === "Text" ? escapeStringValue(part.text, true) : `{${this.printExpr(part.expression, indent)}}`)).join("");
|
|
360
|
+
return `$"${body}"`;
|
|
361
|
+
}
|
|
362
|
+
printMatch(expr, indent) {
|
|
363
|
+
const pad = indentStr(indent);
|
|
364
|
+
const armPad = indentStr(indent + 1);
|
|
365
|
+
const lines = [`match ${this.printExpr(expr.subject, indent)} {`];
|
|
366
|
+
const armParts = [];
|
|
367
|
+
for (const arm of expr.arms) {
|
|
368
|
+
const pattern = arm.pattern.kind === "WildcardPattern" ? "_" : arm.pattern.kind === "RegexPattern" ? `r"${arm.pattern.source}"` : arm.pattern.values.map((v) => this.printExpr(v, indent)).join(", ");
|
|
369
|
+
this.emit(armParts, arm, `${armPad}${pattern} => ${this.printExpr(arm.result, indent + 1)},`, indent + 1);
|
|
370
|
+
}
|
|
371
|
+
// No trailing comma after the last arm, matching the observed convention.
|
|
372
|
+
if (armParts.length > 0)
|
|
373
|
+
armParts[armParts.length - 1] = armParts[armParts.length - 1].replace(/,$/, "");
|
|
374
|
+
lines.push(...armParts, `${pad}}`);
|
|
375
|
+
return lines.join("\n");
|
|
376
|
+
}
|
|
377
|
+
}
|
package/package.json
CHANGED