rapydscript-ng 0.7.24 → 0.8.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +72 -26
  3. package/bin/package.json +1 -0
  4. package/bin/rapydscript +65 -62
  5. package/editor-plugins/nvim/rapydscript/README.md +184 -0
  6. package/editor-plugins/nvim/rapydscript/bin/rapydscript.so +0 -0
  7. package/editor-plugins/nvim/rapydscript/ftdetect/rapydscript.lua +10 -0
  8. package/editor-plugins/nvim/rapydscript/lua/rapydscript/health.lua +88 -0
  9. package/editor-plugins/nvim/rapydscript/lua/rapydscript/init.lua +279 -0
  10. package/local-agent.md +28 -0
  11. package/package.json +4 -5
  12. package/release/baselib-plain-pretty.js +448 -326
  13. package/release/baselib-plain-ugly.js +3 -3
  14. package/release/compiler.js +2528 -1672
  15. package/release/signatures.json +17 -17
  16. package/src/ast.pyj +73 -25
  17. package/src/baselib-builtins.pyj +2 -2
  18. package/src/baselib-containers.pyj +153 -151
  19. package/src/baselib-internal.pyj +3 -3
  20. package/src/baselib-str.pyj +5 -5
  21. package/src/lib/aes.pyj +1 -1
  22. package/src/lib/encodings.pyj +1 -1
  23. package/src/lib/pythonize.pyj +1 -1
  24. package/src/lib/re.pyj +2 -2
  25. package/src/lib/traceback.pyj +165 -28
  26. package/src/lib/uuid.pyj +1 -1
  27. package/src/output/codegen.pyj +10 -3
  28. package/src/output/functions.pyj +31 -40
  29. package/src/output/literals.pyj +11 -14
  30. package/src/output/loops.pyj +25 -87
  31. package/src/output/modules.pyj +5 -47
  32. package/src/output/statements.pyj +1 -1
  33. package/src/output/stream.pyj +58 -31
  34. package/src/parse.pyj +777 -427
  35. package/src/tokenizer.pyj +16 -4
  36. package/src/utils.pyj +1 -1
  37. package/test/_treeshake_mod.pyj +30 -0
  38. package/test/_treeshake_side_effect.pyj +9 -0
  39. package/test/ast_serialization.pyj +392 -0
  40. package/test/async.pyj +95 -0
  41. package/test/baselib.pyj +1 -2
  42. package/test/embedded_compiler.pyj +57 -0
  43. package/test/fmt.pyj +201 -0
  44. package/test/generic.pyj +7 -3
  45. package/test/imports.pyj +8 -6
  46. package/test/internationalization.pyj +38 -37
  47. package/test/lint.pyj +120 -117
  48. package/test/lsp.pyj +222 -0
  49. package/test/repl.pyj +70 -65
  50. package/test/sourcemaps.pyj +315 -0
  51. package/test/starargs.pyj +46 -39
  52. package/test/traceback.pyj +263 -0
  53. package/test/treeshaking.pyj +181 -0
  54. package/test/web_repl_export.pyj +88 -0
  55. package/tools/ast_serialize.mjs +557 -0
  56. package/tools/cli.mjs +510 -0
  57. package/tools/compile.mjs +201 -0
  58. package/tools/compiler.mjs +127 -0
  59. package/tools/{completer.js → completer.mjs} +6 -6
  60. package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
  61. package/tools/export.js +109 -56
  62. package/tools/fmt.mjs +735 -0
  63. package/tools/{gettext.js → gettext.mjs} +46 -53
  64. package/tools/{ini.js → ini.mjs} +9 -10
  65. package/tools/{lint.js → lint.mjs} +78 -55
  66. package/tools/lsp.mjs +914 -0
  67. package/tools/lsp_protocol.mjs +259 -0
  68. package/tools/lsp_symbols.mjs +418 -0
  69. package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
  70. package/tools/{repl.js → repl.mjs} +52 -41
  71. package/tools/{self.js → self.mjs} +56 -46
  72. package/tools/sourcemap.mjs +123 -0
  73. package/tools/test.mjs +152 -0
  74. package/tools/treeshake.mjs +400 -0
  75. package/tools/{utils.js → utils.mjs} +26 -23
  76. package/tools/{web_repl.js → web_repl.mjs} +15 -13
  77. package/tools/web_repl_export.mjs +93 -0
  78. package/tree-sitter/README.md +101 -0
  79. package/tree-sitter/grammar.js +992 -0
  80. package/tree-sitter/package.json +43 -0
  81. package/tree-sitter/queries/rapydscript/highlights.scm +232 -0
  82. package/tree-sitter/queries/rapydscript/indents.scm +64 -0
  83. package/tree-sitter/queries/rapydscript/injections.scm +14 -0
  84. package/tree-sitter/queries/rapydscript/locals.scm +108 -0
  85. package/tree-sitter/src/grammar.json +4976 -0
  86. package/tree-sitter/src/node-types.json +2901 -0
  87. package/tree-sitter/src/parser.c +196465 -0
  88. package/tree-sitter/src/scanner.c +294 -0
  89. package/tree-sitter/src/tree_sitter/alloc.h +54 -0
  90. package/tree-sitter/src/tree_sitter/array.h +330 -0
  91. package/tree-sitter/src/tree_sitter/parser.h +286 -0
  92. package/tree-sitter/test/corpus/chaining.txt +99 -0
  93. package/tree-sitter/test/corpus/classes.txt +147 -0
  94. package/tree-sitter/test/corpus/comprehensions.txt +155 -0
  95. package/tree-sitter/test/corpus/containers.txt +242 -0
  96. package/tree-sitter/test/corpus/control_flow.txt +361 -0
  97. package/tree-sitter/test/corpus/decorators.txt +64 -0
  98. package/tree-sitter/test/corpus/existential.txt +102 -0
  99. package/tree-sitter/test/corpus/functions.txt +293 -0
  100. package/tree-sitter/test/corpus/imports.txt +117 -0
  101. package/tree-sitter/test/corpus/literals.txt +135 -0
  102. package/tree-sitter/test/corpus/operators.txt +296 -0
  103. package/tree-sitter/test/corpus/statements.txt +189 -0
  104. package/tree-sitter/test/corpus/strings.txt +90 -0
  105. package/tree-sitter/test/corpus/subscripts.txt +227 -0
  106. package/tree-sitter/tree-sitter.json +36 -0
  107. package/web-repl/env.js +35 -23
  108. package/web-repl/main.js +8 -8
  109. package/bin/export +0 -75
  110. package/bin/web-repl-export +0 -102
  111. package/tools/cli.js +0 -523
  112. package/tools/compile.js +0 -184
  113. package/tools/compiler.js +0 -84
  114. package/tools/test.js +0 -110
package/tools/fmt.mjs ADDED
@@ -0,0 +1,735 @@
1
+ /* vim:fileencoding=utf-8
2
+ *
3
+ * fmt.mjs -- A PEP8 style code formatter for RapydScript source code.
4
+ *
5
+ * Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
6
+ *
7
+ * Distributed under terms of the BSD license.
8
+ *
9
+ * The formatter deliberately does not reuse the compiler's tokenizer because
10
+ * that tokenizer is lossy for a formatter: it decodes string escapes, converts
11
+ * numbers to JavaScript values, turns regexps into RegExp objects, rewrites
12
+ * f-strings by mutating the source buffer and maps operators (and -> &&, is ->
13
+ * === etc.). Instead we use a small, purpose built, loss-less lexer that
14
+ * preserves the exact source lexeme of every token.
15
+ *
16
+ * RapydScript is close to, but not identical to, Python. In particular it
17
+ * supports multi-line anonymous functions, leading-dot call chaining, verbatim
18
+ * JavaScript string/regex literals and newline sensitive blocks. Reformatting
19
+ * these constructs incorrectly would change program semantics, so the formatter
20
+ * is conservative: "complex" logical lines (those containing anonymous
21
+ * functions, block colons that span lines, semicolon inlined blocks, leading
22
+ * dot chains, backslash continuations or multi-line string/regex literals) have
23
+ * their spacing/indentation/quotes normalized but their physical line structure
24
+ * preserved -- they are never reflowed onto a single line nor wrapped.
25
+ */
26
+ "use strict";
27
+
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Lexer
33
+ // ---------------------------------------------------------------------------
34
+
35
+ var ATOMS = new Set(['True', 'False', 'None']);
36
+ // Keywords that stay keywords (word operators like and/or/not/in/is/new/del are
37
+ // classified as operators, mirroring the compiler's tokenizer).
38
+ var KEYWORDS = new Set([
39
+ 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'do',
40
+ 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
41
+ 'nonlocal', 'pass', 'raise', 'return', 'yield', 'try', 'while', 'with',
42
+ ]);
43
+ var WORD_OPS = new Set(['and', 'or', 'not', 'in', 'is', 'new', 'del', 'void', 'typeof', 'instanceof']);
44
+ // After these tokens a `/` begins a regular expression rather than division.
45
+ var KW_BEFORE_EXPR = new Set(['return', 'yield', 'raise', 'elif', 'else', 'if', 'await', 'in', 'assert', 'while']);
46
+ var PUNC_BEFORE_EXPR = new Set(['[', '{', '(', ',', '.', ';', ':']);
47
+ var PUNC_CHARS = new Set(['(', ')', '[', ']', '{', '}', ',', ';', ':', '?']);
48
+ var STRING_MOD = /^[vrufVRUF]+$/;
49
+
50
+ // Symbolic operators, ordered longest-first for greedy matching.
51
+ var OPERATORS = [
52
+ '>>>=', '>>>', '>>=', '<<=', '//=', '**=', '>>', '<<', '//', '**', '<=',
53
+ '>=', '==', '!=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->',
54
+ '~', '+', '-', '*', '/', '%', '<', '>', '=', '&', '|', '^', '@', '!',
55
+ ];
56
+ var OPERATOR_CHARS = new Set('+-*&%=<>!?|~^@/'.split(''));
57
+
58
+ function is_ws(c) { return c === ' ' || c === '\t' || c === '\f' || c === '\v' || c === ' '; }
59
+ function is_digit(c) { return c >= '0' && c <= '9'; }
60
+ function is_ident_start(c) {
61
+ if (c === '_' || c === '$') return true;
62
+ var k = c.charCodeAt(0);
63
+ return (k >= 65 && k <= 90) || (k >= 97 && k <= 122) || k > 127;
64
+ }
65
+ function is_ident_char(c) { return is_ident_start(c) || is_digit(c); }
66
+
67
+ function FormatError(message, line) {
68
+ var e = new Error(message + (line ? ' (line ' + line + ')' : ''));
69
+ e.is_format_error = true;
70
+ return e;
71
+ }
72
+
73
+ function lex(text) {
74
+ var n = text.length;
75
+ var pos = 0, line = 1;
76
+ var toks = [];
77
+ var prev_sig = null; // previous non-comment token, for regex/kind decisions
78
+
79
+ function regex_allowed() {
80
+ if (!prev_sig) return true;
81
+ if (prev_sig.type === 'op') return true;
82
+ if (prev_sig.type === 'keyword' && KW_BEFORE_EXPR.has(prev_sig.value)) return true;
83
+ if (prev_sig.type === 'punc' && PUNC_BEFORE_EXPR.has(prev_sig.value)) return true;
84
+ return false;
85
+ }
86
+
87
+ function consume_ws() {
88
+ var nlb = 0, had_ws = false, cont = false;
89
+ while (pos < n) {
90
+ var c = text[pos];
91
+ if (c === '\\' && text[pos + 1] === '\n') { pos += 2; line++; nlb++; cont = true; continue; }
92
+ if (c === '\n') { nlb++; line++; pos++; had_ws = true; continue; }
93
+ if (is_ws(c)) { had_ws = true; pos++; continue; }
94
+ break;
95
+ }
96
+ // Leading whitespace of the current physical line.
97
+ var ls = pos;
98
+ while (ls > 0 && text[ls - 1] !== '\n') ls--;
99
+ var ie = ls;
100
+ while (ie < n && is_ws(text[ie])) ie++;
101
+ return { nlb: nlb, sp: (had_ws && nlb === 0), cont: cont, indent: text.slice(ls, ie) };
102
+ }
103
+
104
+ function read_string_body(qpos) {
105
+ // qpos points at the opening quote (prefix already consumed). Advances pos
106
+ // past the closing quote and returns nothing; caller slices the lexeme.
107
+ var q = text[qpos];
108
+ if (text[qpos + 1] === q && text[qpos + 2] === q) {
109
+ pos = qpos + 3;
110
+ while (pos < n) {
111
+ if (text[pos] === '\\') { pos += 2; continue; }
112
+ if (text[pos] === q && text[pos + 1] === q && text[pos + 2] === q) { pos += 3; return; }
113
+ if (text[pos] === '\n') line++;
114
+ pos++;
115
+ }
116
+ throw FormatError('Unterminated triple-quoted string', line);
117
+ }
118
+ pos = qpos + 1;
119
+ while (pos < n) {
120
+ var c = text[pos];
121
+ if (c === '\\') { pos += 2; continue; }
122
+ if (c === q) { pos++; return; }
123
+ if (c === '\n') throw FormatError('End of line while scanning string literal', line);
124
+ pos++;
125
+ }
126
+ throw FormatError('Unterminated string', line);
127
+ }
128
+
129
+ function read_regexp() {
130
+ var start = pos;
131
+ pos++; // first '/'
132
+ if (text[pos] === '/') {
133
+ pos++;
134
+ if (text[pos] === '/') {
135
+ pos++; // verbose ///.../// regex
136
+ while (pos < n) {
137
+ if (text[pos] === '\\') { pos += 2; continue; }
138
+ if (text[pos] === '/' && text[pos + 1] === '/' && text[pos + 2] === '/') { pos += 3; break; }
139
+ if (text[pos] === '\n') line++;
140
+ pos++;
141
+ }
142
+ }
143
+ // else: empty regexp //, nothing more to read before modifiers
144
+ } else {
145
+ var in_class = false;
146
+ while (pos < n) {
147
+ var c = text[pos];
148
+ if (c === '\\') { pos += 2; continue; }
149
+ if (c === '[') { in_class = true; pos++; continue; }
150
+ if (c === ']' && in_class) { in_class = false; pos++; continue; }
151
+ if (c === '/' && !in_class) { pos++; break; }
152
+ if (c === '\n') { throw FormatError('Unterminated regular expression', line); }
153
+ pos++;
154
+ }
155
+ }
156
+ while (pos < n && /[a-zA-Z]/.test(text[pos])) pos++;
157
+ return text.slice(start, pos);
158
+ }
159
+
160
+ function read_number(from_dot) {
161
+ var start = pos;
162
+ if (!from_dot && text[pos] === '0' && (text[pos + 1] === 'x' || text[pos + 1] === 'X')) {
163
+ pos += 2;
164
+ while (pos < n && /[0-9a-fA-F]/.test(text[pos])) pos++;
165
+ } else if (!from_dot && text[pos] === '0' && (text[pos + 1] === 'b' || text[pos + 1] === 'B')) {
166
+ pos += 2;
167
+ while (pos < n && (text[pos] === '0' || text[pos] === '1')) pos++;
168
+ } else {
169
+ if (from_dot) { pos++; while (pos < n && is_digit(text[pos])) pos++; }
170
+ else {
171
+ while (pos < n && is_digit(text[pos])) pos++;
172
+ if (text[pos] === '.') { pos++; while (pos < n && is_digit(text[pos])) pos++; }
173
+ }
174
+ if (text[pos] === 'e' || text[pos] === 'E') {
175
+ pos++;
176
+ if (text[pos] === '+' || text[pos] === '-') pos++;
177
+ while (pos < n && is_digit(text[pos])) pos++;
178
+ }
179
+ }
180
+ return text.slice(start, pos);
181
+ }
182
+
183
+ function match_operator() {
184
+ for (var i = 0; i < OPERATORS.length; i++) {
185
+ if (text.startsWith(OPERATORS[i], pos)) return OPERATORS[i];
186
+ }
187
+ return text[pos];
188
+ }
189
+
190
+ function push(type, value, ws) {
191
+ var t = {
192
+ type: type, value: value, nlb: ws.nlb, sp: ws.sp, cont: ws.cont,
193
+ line_indent: ws.indent, line: line,
194
+ };
195
+ toks.push(t);
196
+ if (type !== 'comment' && type !== 'shebang') prev_sig = t;
197
+ return t;
198
+ }
199
+
200
+ while (pos < n) {
201
+ var ws = consume_ws();
202
+ if (pos >= n) break;
203
+ var c = text[pos];
204
+
205
+ if (c === '#') {
206
+ var c_start = pos;
207
+ var eol = text.indexOf('\n', pos);
208
+ if (eol === -1) eol = n;
209
+ var raw = text.slice(pos, eol);
210
+ pos = eol;
211
+ if (c_start === 0 && raw[1] === '!') push('shebang', raw, ws);
212
+ else push('comment', raw, ws);
213
+ continue;
214
+ }
215
+ if (c === '"' || c === "'") {
216
+ var s = pos;
217
+ read_string_body(pos);
218
+ push('string', text.slice(s, pos), ws);
219
+ continue;
220
+ }
221
+ if (is_ident_start(c)) {
222
+ var ws_start = pos;
223
+ while (pos < n && is_ident_char(text[pos])) pos++;
224
+ var word = text.slice(ws_start, pos);
225
+ if (STRING_MOD.test(word) && (text[pos] === '"' || text[pos] === "'")) {
226
+ var qpos = pos;
227
+ read_string_body(pos);
228
+ push('string', word + text.slice(qpos, pos), ws);
229
+ continue;
230
+ }
231
+ if (ATOMS.has(word)) push('atom', word, ws);
232
+ else if (WORD_OPS.has(word)) push('op', word, ws);
233
+ else if (KEYWORDS.has(word)) push('keyword', word, ws);
234
+ else push('name', word, ws);
235
+ continue;
236
+ }
237
+ if (is_digit(c)) { push('number', read_number(false), ws); continue; }
238
+ if (c === '.') {
239
+ if (is_digit(text[pos + 1])) { push('number', read_number(true), ws); continue; }
240
+ pos++; push('punc', '.', ws); continue;
241
+ }
242
+ if (c === '/') {
243
+ if (regex_allowed()) { push('regexp', read_regexp(), ws); continue; }
244
+ var op = match_operator(); pos += op.length; push('op', op, ws); continue;
245
+ }
246
+ if (PUNC_CHARS.has(c)) { pos++; push('punc', c, ws); continue; }
247
+ if (OPERATOR_CHARS.has(c)) { var op2 = match_operator(); pos += op2.length; push('op', op2, ws); continue; }
248
+ throw FormatError('Unexpected character «' + c + '»', line);
249
+ }
250
+
251
+ push('eof', '', { nlb: 0, sp: false, cont: false, indent: '' });
252
+ return toks;
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Token rendering (spacing engine)
257
+ // ---------------------------------------------------------------------------
258
+
259
+ function is_operand(p) {
260
+ if (!p) return false;
261
+ if (p.type === 'name' || p.type === 'number' || p.type === 'string' || p.type === 'atom' || p.type === 'regexp') return true;
262
+ return p.value === ')' || p.value === ']' || p.value === '}';
263
+ }
264
+
265
+ function is_call_paren(p) {
266
+ if (!p) return false;
267
+ if (p.type === 'name' || p.type === 'string' || p.type === 'atom') return true;
268
+ if (p.value === 'def') return true;
269
+ return p.value === ')' || p.value === ']' || p.value === '}';
270
+ }
271
+
272
+ function is_index_bracket(p) {
273
+ if (!p) return false;
274
+ if (p.type === 'name' || p.type === 'string' || p.type === 'number' || p.type === 'atom') return true;
275
+ return p.value === ')' || p.value === ']' || p.value === '}';
276
+ }
277
+
278
+ function is_unary_here(p, t) {
279
+ if (t.type === 'op') {
280
+ var v = t.value;
281
+ if (v === '+' || v === '-' || v === '~' || v === '*' || v === '**') return !is_operand(p);
282
+ if (v === '@' && p === null) return true;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ function separator(p, t, stack, prev_was_unary, sig_count, first_at) {
288
+ var pv = p.value, tv = t.value, pty = p.type, tty = t.type;
289
+ var top = stack.length ? stack[stack.length - 1] : null;
290
+ if (pty === 'punc' && (pv === '(' || pv === '[' || pv === '{')) return false;
291
+ if (tty === 'punc' && (tv === ')' || tv === ']' || tv === '}')) return false;
292
+ if (tv === ',' || tv === ';') return false;
293
+ if (tv === '.' || pv === '.') return false;
294
+ if (tv === '?' || pv === '?') return !!t.sp; // existential: preserve source spacing
295
+ if (tv === ':') return false;
296
+ if (pv === ':') { if (top && top.kind === 'index') return false; return true; }
297
+ if (pv === '@' && first_at && sig_count === 1) return false; // decorator
298
+ if (tv === '(' && is_call_paren(p)) return false;
299
+ if (tv === '[' && is_index_bracket(p)) return false;
300
+ if ((tv === '=' || pv === '=') && top && top.kind === 'call') return false; // kwarg / default
301
+ if (prev_was_unary) return false;
302
+ return true;
303
+ }
304
+
305
+ function format_comment(raw) {
306
+ var m = raw.match(/^#+/)[0];
307
+ var rest = raw.slice(m.length).replace(/\s+$/, '');
308
+ if (rest === '') return m;
309
+ if (rest[0] !== ' ' && rest[0] !== '!') rest = ' ' + rest;
310
+ return m + rest;
311
+ }
312
+
313
+ function normalize_quotes(lex_val, preferred) {
314
+ var i = 0;
315
+ while (i < lex_val.length && /[vrufVRUF]/.test(lex_val[i])) i++;
316
+ var prefix = lex_val.slice(0, i);
317
+ var q = lex_val[i];
318
+ if (q !== "'" && q !== '"') return lex_val;
319
+ var lower = prefix.toLowerCase();
320
+ if (lower.indexOf('v') >= 0 || lower.indexOf('r') >= 0 || lower.indexOf('f') >= 0) return lex_val;
321
+ if (lex_val[i + 1] === q && lex_val[i + 2] === q) return lex_val; // triple-quoted: leave as-is
322
+ var target = preferred;
323
+ if (q === target) return lex_val;
324
+ var body = lex_val.slice(i + 1, lex_val.length - 1);
325
+ var nb = '', j = 0;
326
+ while (j < body.length) {
327
+ var c = body[j];
328
+ if (c === '\\' && j + 1 < body.length) {
329
+ var nx = body[j + 1];
330
+ if (nx === q) { nb += q; j += 2; continue; } // escaped old quote -> unescape
331
+ nb += '\\' + nx; j += 2; continue;
332
+ }
333
+ if (c === target) { nb += '\\' + target; j++; continue; } // must escape target
334
+ nb += c; j++;
335
+ }
336
+ var count = function (s) { var k = 0; for (var x = 0; x < s.length; x++) if (s[x] === '\\') k++; return k; };
337
+ if (count(nb) > count(body)) return lex_val; // don't increase escapes
338
+ return prefix + target + nb + target;
339
+ }
340
+
341
+ function token_text(t, opts) {
342
+ if (t.type === 'string') return normalize_quotes(t.value, opts.preferred);
343
+ return t.value;
344
+ }
345
+
346
+ // Render a list of tokens to a string. When honor_breaks is true, source line
347
+ // breaks (nlb>0) are preserved as newlines + re-based indentation; otherwise
348
+ // everything is emitted on one line (reflow).
349
+ function render(toks, honor_breaks, src_base, new_base, opts, initial_stack) {
350
+ var out = '';
351
+ var stack = initial_stack ? initial_stack.slice() : [];
352
+ var prev_sig = null, prev_was_unary = false, sig_count = 0, first_at = false;
353
+ for (var k = 0; k < toks.length; k++) {
354
+ var t = toks[k];
355
+ var did_break = false;
356
+ if (k > 0 && honor_breaks && t.nlb > 0) {
357
+ out += (t.cont ? ' \\\n' : '\n');
358
+ var li = t.line_indent || '';
359
+ // Rebase indentation that shares the statement's base prefix; preserve
360
+ // dedented continuation lines verbatim (e.g. a leading-dot line that
361
+ // binds to an outer block, or a `.while` clause of a do/while loop).
362
+ if (li.startsWith(src_base)) out += new_base + li.slice(src_base.length);
363
+ else out += li;
364
+ did_break = true;
365
+ }
366
+ if (t.type === 'comment' || t.type === 'shebang') {
367
+ var ctext = (t.type === 'shebang') ? t.value : format_comment(t.value);
368
+ if (k === 0 || did_break) out += ctext;
369
+ else out += ' ' + ctext;
370
+ continue;
371
+ }
372
+ if (k > 0 && !did_break && prev_sig !== null) {
373
+ if (separator(prev_sig, t, stack, prev_was_unary, sig_count, first_at)) out += ' ';
374
+ }
375
+ out += token_text(t, opts);
376
+ prev_was_unary = is_unary_here(prev_sig, t);
377
+ if (t.type === 'punc') {
378
+ if (t.value === '(') stack.push({ kind: is_call_paren(prev_sig) ? 'call' : 'group' });
379
+ else if (t.value === '[') stack.push({ kind: is_index_bracket(prev_sig) ? 'index' : 'list' });
380
+ else if (t.value === '{') stack.push({ kind: 'dict' });
381
+ else if (t.value === ')' || t.value === ']' || t.value === '}') { if (stack.length) stack.pop(); }
382
+ }
383
+ if (sig_count === 0 && t.value === '@' && t.type === 'op') first_at = true;
384
+ prev_sig = t; sig_count++;
385
+ }
386
+ return out;
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Line wrapping (conservative)
391
+ // ---------------------------------------------------------------------------
392
+
393
+ function wrap_line(code, level, opts) {
394
+ // Locate the top-level (depth 0 -> 1) bracket group with commas and the
395
+ // largest span, then explode it across multiple lines with hanging indent.
396
+ var depth = 0, open_stack = [], groups = [];
397
+ for (var i = 0; i < code.length; i++) {
398
+ var v = code[i].value, ty = code[i].type;
399
+ if (ty === 'punc' && (v === '(' || v === '[' || v === '{')) { if (depth === 0) open_stack.push({ open: i, ch: v }); depth++; }
400
+ else if (ty === 'punc' && (v === ')' || v === ']' || v === '}')) { depth--; if (depth === 0 && open_stack.length) { var g = open_stack.pop(); g.close = i; groups.push(g); } }
401
+ }
402
+ var best = null;
403
+ for (var gi = 0; gi < groups.length; gi++) {
404
+ var grp = groups[gi];
405
+ if (grp.close === undefined) continue;
406
+ var d = 0, has_comma = false, is_comprehension = false;
407
+ for (var x = grp.open + 1; x < grp.close; x++) {
408
+ var vv = code[x].value, tt = code[x].type;
409
+ if (tt === 'punc' && (vv === '(' || vv === '[' || vv === '{')) d++;
410
+ else if (tt === 'punc' && (vv === ')' || vv === ']' || vv === '}')) d--;
411
+ else if (d === 0 && tt === 'punc' && vv === ',') has_comma = true;
412
+ else if (d === 0 && tt === 'keyword' && vv === 'for') is_comprehension = true;
413
+ }
414
+ // In a comprehension/generator the top-level commas are tuple unpacking,
415
+ // not element separators, so it is not safe to explode on them.
416
+ if (!has_comma || is_comprehension) continue;
417
+ var span = grp.close - grp.open;
418
+ if (!best || span > best.span) best = { open: grp.open, close: grp.close, ch: grp.ch, span: span };
419
+ }
420
+ if (!best) return null;
421
+
422
+ var base_indent = ' '.repeat(4 * level);
423
+ var child_indent = ' '.repeat(4 * (level + 1));
424
+ var open_ch = best.ch;
425
+ var seed_kind = (open_ch === '(') ? (is_call_paren(best.open > 0 ? code[best.open - 1] : null) ? 'call' : 'group')
426
+ : (open_ch === '[') ? (is_index_bracket(best.open > 0 ? code[best.open - 1] : null) ? 'index' : 'list')
427
+ : 'dict';
428
+
429
+ var head = code.slice(0, best.open + 1);
430
+ var head_str = base_indent + render(head, false, '', base_indent, opts);
431
+
432
+ var inner = code.slice(best.open + 1, best.close);
433
+ var elems = [], cur = [], dd = 0;
434
+ for (var y = 0; y < inner.length; y++) {
435
+ var tk = inner[y], iv = tk.value, it = tk.type;
436
+ if (it === 'punc' && (iv === '(' || iv === '[' || iv === '{')) { dd++; cur.push(tk); }
437
+ else if (it === 'punc' && (iv === ')' || iv === ']' || iv === '}')) { dd--; cur.push(tk); }
438
+ else if (dd === 0 && it === 'punc' && iv === ',') { elems.push(cur); cur = []; }
439
+ else cur.push(tk);
440
+ }
441
+ if (cur.length) elems.push(cur);
442
+ elems = elems.filter(function (e) { return e.length; });
443
+ if (!elems.length) return null;
444
+
445
+ var elem_lines = elems.map(function (e) { return child_indent + render(e, false, '', child_indent, opts, [{ kind: seed_kind }]); });
446
+ var body = elem_lines.join(',\n');
447
+ if (open_ch === '[' || open_ch === '{') body += ','; // magic trailing comma for literals only
448
+
449
+ var tail = code.slice(best.close);
450
+ var tail_str = base_indent + render(tail, false, '', base_indent, opts);
451
+ return head_str + '\n' + body + '\n' + tail_str;
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Grouping into logical lines / elements
456
+ // ---------------------------------------------------------------------------
457
+
458
+ function make_stmt(tokens) {
459
+ var sig = tokens.filter(function (t) { return t.type !== 'comment' && t.type !== 'shebang'; });
460
+ var first_sig = sig[0], last_sig = sig[sig.length - 1];
461
+ var has_def_or_class = sig.some(function (t) { return t.type === 'keyword' && (t.value === 'def' || t.value === 'class'); });
462
+ var has_semicolon = sig.some(function (t) { return t.value === ';'; });
463
+ // A logical line that starts with `.` or that contains a physical line
464
+ // beginning with `.` is a leading-dot chain (or a do/while `.while` clause).
465
+ // These bind to an outer block by indentation, so we preserve their layout.
466
+ var leading_dot = (first_sig && first_sig.value === '.') ||
467
+ tokens.some(function (t, i) { return i > 0 && t.nlb > 0 && t.type === 'punc' && t.value === '.'; });
468
+ var used_backslash = tokens.some(function (t) { return t.cont; });
469
+ var multi_physical = tokens.some(function (t, i) { return i > 0 && t.nlb > 0; });
470
+ var colon_then_newline = false;
471
+ for (var i = 0; i < tokens.length - 1; i++) { if (tokens[i].value === ':' && tokens[i + 1].nlb > 0) { colon_then_newline = true; break; } }
472
+ var has_multiline_token = tokens.some(function (t) { return typeof t.value === 'string' && t.value.indexOf('\n') >= 0; });
473
+ var interior_comment = false;
474
+ for (var j = 0; j < tokens.length; j++) {
475
+ if (tokens[j].type === 'comment') {
476
+ if (j < tokens.length - 1) interior_comment = true;
477
+ else if (tokens[j].nlb > 0) interior_comment = true;
478
+ }
479
+ }
480
+ var complex = leading_dot || used_backslash || interior_comment || has_def_or_class ||
481
+ has_semicolon || has_multiline_token || (multi_physical && colon_then_newline);
482
+
483
+ var inline_body_colon = false, d = 0;
484
+ for (var s = 0; s < sig.length; s++) {
485
+ var v = sig[s].value, ty = sig[s].type;
486
+ if (ty === 'punc' && (v === '(' || v === '[' || v === '{')) d++;
487
+ else if (ty === 'punc' && (v === ')' || v === ']' || v === '}')) d--;
488
+ else if (d === 0 && v === ':' && s < sig.length - 1) inline_body_colon = true;
489
+ }
490
+ var wrappable = !complex && !has_def_or_class && !has_semicolon && !inline_body_colon;
491
+ var is_def_like = first_sig && ((first_sig.type === 'keyword' && (first_sig.value === 'def' || first_sig.value === 'class')) || first_sig.value === 'async');
492
+ var is_decorator = first_sig && first_sig.value === '@';
493
+ var ends_block = last_sig && last_sig.value === ':';
494
+ return {
495
+ kind: 'stmt', tokens: tokens, indent: tokens[0].line_indent || '',
496
+ blank_before: Math.max(0, tokens[0].nlb - 1), complex: complex, wrappable: wrappable,
497
+ is_def_like: !!is_def_like, is_decorator: !!is_decorator, ends_block: !!ends_block, level: 0,
498
+ };
499
+ }
500
+
501
+ function group(toks) {
502
+ var elements = [];
503
+ var cur = [], depth = 0;
504
+ function flush() { if (cur.length) { elements.push(make_stmt(cur)); cur = []; } }
505
+ for (var k = 0; k < toks.length; k++) {
506
+ var t = toks[k];
507
+ if (t.type === 'eof') break;
508
+ var is_boundary = depth === 0 && cur.length > 0 && t.nlb > 0 && !t.cont && !(t.value === '.' && t.type === 'punc');
509
+ if (is_boundary) flush();
510
+ if (depth === 0 && cur.length === 0 && (t.type === 'comment' || t.type === 'shebang') && (t.nlb > 0 || k === 0)) {
511
+ elements.push({
512
+ kind: 'comment', value: t.value, is_shebang: t.type === 'shebang',
513
+ indent: t.line_indent || '', blank_before: (k === 0 ? 0 : Math.max(0, t.nlb - 1)),
514
+ level: 0, ends_block: false,
515
+ });
516
+ continue;
517
+ }
518
+ cur.push(t);
519
+ if (t.type === 'punc') {
520
+ if (t.value === '(' || t.value === '[' || t.value === '{') depth++;
521
+ else if (t.value === ')' || t.value === ']' || t.value === '}') depth = Math.max(0, depth - 1);
522
+ }
523
+ }
524
+ flush();
525
+ return elements;
526
+ }
527
+
528
+ function compute_levels(elements) {
529
+ var stack = [{ w: -1 }];
530
+ for (var i = 0; i < elements.length; i++) {
531
+ var el = elements[i];
532
+ if (el.kind !== 'stmt') continue;
533
+ var w = el.indent.length;
534
+ if (w > stack[stack.length - 1].w) stack.push({ w: w });
535
+ else {
536
+ while (stack.length > 1 && w < stack[stack.length - 1].w) stack.pop();
537
+ if (w > stack[stack.length - 1].w) stack.push({ w: w });
538
+ }
539
+ el.level = stack.length - 2;
540
+ if (el.level < 0) el.level = 0;
541
+ }
542
+ // Comments take the level of the following statement (or the previous one at EOF).
543
+ for (var c = 0; c < elements.length; c++) {
544
+ if (elements[c].kind !== 'comment') continue;
545
+ var lvl = null;
546
+ for (var f = c + 1; f < elements.length; f++) { if (elements[f].kind === 'stmt') { lvl = elements[f].level; break; } }
547
+ if (lvl === null) { for (var b = c - 1; b >= 0; b--) { if (elements[b].kind === 'stmt') { lvl = elements[b].level; break; } } }
548
+ elements[c].level = lvl === null ? 0 : lvl;
549
+ }
550
+ }
551
+
552
+ // ---------------------------------------------------------------------------
553
+ // Blank line policy helpers
554
+ // ---------------------------------------------------------------------------
555
+
556
+ function leads_def(elements, idx) {
557
+ var j = idx;
558
+ while (j < elements.length) {
559
+ var e = elements[j];
560
+ if (e.kind === 'comment') {
561
+ if (j + 1 < elements.length && elements[j + 1].blank_before === 0) { j++; continue; }
562
+ return false;
563
+ }
564
+ if (e.kind === 'stmt') {
565
+ if (e.is_decorator) { if (j + 1 < elements.length && elements[j + 1].blank_before === 0) { j++; continue; } return false; }
566
+ return !!e.is_def_like;
567
+ }
568
+ return false;
569
+ }
570
+ return false;
571
+ }
572
+
573
+ function is_visual_def_start(elements, idx) {
574
+ if (!leads_def(elements, idx)) return false;
575
+ if (idx === 0) return true;
576
+ var prev = elements[idx - 1], el = elements[idx];
577
+ if (el.blank_before === 0 && (prev.kind === 'comment' || (prev.kind === 'stmt' && prev.is_decorator))) return false;
578
+ return true;
579
+ }
580
+
581
+ // ---------------------------------------------------------------------------
582
+ // Element rendering + assembly
583
+ // ---------------------------------------------------------------------------
584
+
585
+ function render_stmt(el, opts) {
586
+ var base_indent = ' '.repeat(4 * el.level);
587
+ if (el.complex) {
588
+ return base_indent + render(el.tokens, true, el.indent, base_indent, opts);
589
+ }
590
+ var comment = null, code = el.tokens;
591
+ if (code.length && code[code.length - 1].type === 'comment') { comment = code[code.length - 1]; code = code.slice(0, -1); }
592
+ var line = render(code, false, el.indent, base_indent, opts);
593
+ var full = base_indent + line;
594
+ var comment_str = comment ? ' ' + format_comment(comment.value) : '';
595
+ if ((full.length + comment_str.length) > opts.line_length && el.wrappable) {
596
+ var wrapped = wrap_line(code, el.level, opts);
597
+ if (wrapped !== null) return comment_str ? wrapped + comment_str : wrapped;
598
+ }
599
+ return full + comment_str;
600
+ }
601
+
602
+ export function format_string(src, options) {
603
+ var opts = normalize_opts(options);
604
+ var text = src.replace(/\r\n?/g, '\n');
605
+ var toks = lex(text);
606
+ var elements = group(toks);
607
+ compute_levels(elements);
608
+
609
+ var result = '';
610
+ for (var idx = 0; idx < elements.length; idx++) {
611
+ var el = elements[idx];
612
+ var etext = (el.kind === 'comment')
613
+ ? (' '.repeat(4 * el.level) + (el.is_shebang ? el.value : format_comment(el.value)))
614
+ : render_stmt(el, opts);
615
+ var blanks = 0;
616
+ if (idx > 0) {
617
+ var prev = elements[idx - 1];
618
+ var cap = (el.level === 0) ? 2 : 1;
619
+ blanks = Math.min(el.blank_before, cap);
620
+ if (prev.ends_block) blanks = 0;
621
+ if (is_visual_def_start(elements, idx)) {
622
+ var req = (el.level === 0) ? 2 : 1;
623
+ if (!prev.ends_block) blanks = Math.max(blanks, req);
624
+ }
625
+ }
626
+ if (idx === 0) result += etext;
627
+ else result += '\n' + '\n'.repeat(blanks) + etext;
628
+ }
629
+ if (result === '') return '';
630
+ return result + '\n';
631
+ }
632
+
633
+ function normalize_opts(options) {
634
+ options = options || {};
635
+ var ll = options.line_length;
636
+ if (typeof ll === 'string') ll = parseInt(ll, 10);
637
+ if (!ll || isNaN(ll) || ll < 1) ll = 80;
638
+ var pref = options.preferred;
639
+ if (!pref) {
640
+ var pq = options.preferred_quote;
641
+ pref = (pq === 'double' || pq === '"') ? '"' : "'";
642
+ }
643
+ return { line_length: ll, preferred: pref };
644
+ }
645
+
646
+ // ---------------------------------------------------------------------------
647
+ // check-only reporting
648
+ // ---------------------------------------------------------------------------
649
+
650
+ export function check_report(file, src, formatted, options) {
651
+ var opts = normalize_opts(options);
652
+ var errs = [];
653
+ if (formatted !== src.replace(/\r\n?/g, '\n')) errs.push(file + ': would be reformatted');
654
+ var lines = formatted.split('\n');
655
+ for (var i = 0; i < lines.length; i++) {
656
+ if (lines[i].length > opts.line_length) {
657
+ errs.push(file + ':' + (i + 1) + ': line exceeds ' + opts.line_length + ' characters (' + lines[i].length + ')');
658
+ }
659
+ }
660
+ return errs;
661
+ }
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // File collection + CLI
665
+ // ---------------------------------------------------------------------------
666
+
667
+ export async function collect_pyj_files(inputs) {
668
+ var files = [];
669
+ async function walk(list, from_dir) {
670
+ for (var i = 0; i < list.length; i++) {
671
+ var f = list[i];
672
+ var st;
673
+ try { st = await fs.promises.lstat(f); }
674
+ catch (e) { throw new Error("can't access: " + f); }
675
+ if (st.isDirectory()) {
676
+ var children = (await fs.promises.readdir(f)).sort().map(function (x) { return path.join(f, x); });
677
+ await walk(children, true);
678
+ } else if (st.isFile()) {
679
+ if (from_dir) { if (f.endsWith('.pyj')) files.push(f); }
680
+ else files.push(f);
681
+ }
682
+ }
683
+ }
684
+ await walk(inputs, false);
685
+ return files;
686
+ }
687
+
688
+ async function read_stdin() {
689
+ var chunks = [];
690
+ process.stdin.setEncoding('utf-8');
691
+ await new Promise(function (resolve, reject) {
692
+ process.stdin.on('data', function (chunk) { chunks.push(chunk); });
693
+ process.stdin.on('end', resolve);
694
+ process.stdin.on('error', reject);
695
+ });
696
+ return chunks.join('');
697
+ }
698
+
699
+ export async function cli(argv, base_path, src_path, lib_path) {
700
+ var opts = normalize_opts({ line_length: argv.line_length, preferred_quote: argv.preferred_quote });
701
+ var inputs = (argv.files || []).slice();
702
+
703
+ if (inputs.length === 0) {
704
+ var src = await read_stdin();
705
+ var out;
706
+ try { out = format_string(src, opts); }
707
+ catch (e) { console.error('Error formatting stdin: ' + (e.message || e)); process.exit(2); }
708
+ process.stdout.write(out);
709
+ process.exit(0);
710
+ }
711
+
712
+ var files;
713
+ try { files = await collect_pyj_files(inputs); }
714
+ catch (e) { console.error('ERROR: ' + (e.message || e)); process.exit(2); }
715
+
716
+ var had_errors = false;
717
+ for (var i = 0; i < files.length; i++) {
718
+ var f = files[i];
719
+ var code;
720
+ try { code = await fs.promises.readFile(f, 'utf-8'); }
721
+ catch (e) { console.error("ERROR: can't read file: " + f); process.exit(2); }
722
+ var formatted;
723
+ try { formatted = format_string(code, opts); }
724
+ catch (e) { console.error(f + ': ' + (e.message || e)); had_errors = true; continue; }
725
+ if (argv.check_only) {
726
+ var errs = check_report(f, code, formatted, opts);
727
+ if (errs.length) { had_errors = true; errs.forEach(function (m) { console.error(m); }); }
728
+ } else if (formatted !== code.replace(/\r\n?/g, '\n')) {
729
+ try { await fs.promises.writeFile(f, formatted); }
730
+ catch (e) { console.error("ERROR: can't write file: " + f); process.exit(2); }
731
+ console.log('reformatted ' + f);
732
+ }
733
+ }
734
+ process.exit(had_errors ? 1 : 0);
735
+ }