Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/python-wasm
Path: blob/main/python/pylang/tools/repl.ts
1396 views
1
/*
2
* Copyright (C) 2021 William Stein <[email protected]>
3
* Copyright (C) 2015 Kovid Goyal <kovid at kovidgoyal.net>
4
*
5
* Distributed under terms of the BSD license
6
*/
7
8
import { mkdirSync, readFileSync, writeFileSync } from "fs";
9
import { join } from "path";
10
import { runInThisContext } from "vm";
11
import {
12
getImportDirs,
13
colored,
14
importPath,
15
libraryPath,
16
pathExists,
17
} from "./utils";
18
import Completer from "./completer";
19
import { clearLine, createInterface } from "readline";
20
import createCompiler from "./compiler";
21
import { arch } from "os";
22
23
const DEFAULT_HISTORY_SIZE = 1000;
24
const HOME =
25
process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"] ?? "/tmp";
26
27
function expandUser(x: string): string {
28
return x.replace("~", HOME);
29
}
30
31
const CACHEDIR = process.env.XDG_CACHE_HOME
32
? expandUser(process.env.XDG_CACHE_HOME)
33
: join(HOME, ".cache");
34
35
export interface Options {
36
input;
37
output;
38
show_js: boolean;
39
ps1: string;
40
ps2: string;
41
console: Console;
42
terminal: boolean;
43
histfile: string;
44
historySize: number;
45
mockReadline?: Function; // for mocking readline (for testing only)
46
jsage?: boolean; // sage-style preparsing
47
tokens?: boolean; // show very verbose tokens as parsed
48
}
49
50
function replDefaults(options: Partial<Options>): Options {
51
if (!options.input) {
52
options.input = process.stdin;
53
}
54
if (!options.output) {
55
options.output = process.stdout;
56
}
57
if (options.show_js == null) {
58
options.show_js = true;
59
}
60
if (!options.ps1) {
61
if (options.jsage) {
62
options.ps1 = process.stdin.isTTY ? "jsage: " : "";
63
} else {
64
options.ps1 = process.stdin.isTTY ? ">>> " : "";
65
}
66
}
67
if (!options.ps2) {
68
options.ps2 = process.stdin.isTTY ? "... " : "";
69
}
70
if (!options.console) {
71
options.console = console;
72
}
73
if (options.terminal == null) {
74
options.terminal = !!options.output?.isTTY;
75
}
76
if (options.histfile == null) {
77
const CACHE = join(CACHEDIR, "pylang");
78
if (!pathExists(CACHE)) {
79
mkdirSync(CACHE, { recursive: true });
80
}
81
options.histfile = join(CACHE, "history");
82
}
83
options.historySize = options.historySize ?? DEFAULT_HISTORY_SIZE;
84
return options as Options;
85
}
86
87
function readHistory(options: Options): string[] {
88
if (options.histfile) {
89
if (!pathExists(options.histfile)) {
90
return [];
91
}
92
try {
93
return readFileSync(options.histfile, "utf-8").split("\n");
94
} catch (err) {
95
options.console.warn(`Error reading history file - ${err}`);
96
return [];
97
}
98
}
99
return [];
100
}
101
102
function writeHistory(options: Options, history: string[]): void {
103
if (options.histfile) {
104
try {
105
return writeFileSync(options.histfile, history.join("\n"), "utf-8");
106
} catch (err) {
107
options.console.warn(`Error writing history file - ${err}`);
108
}
109
}
110
}
111
112
function createReadlineInterface(options: Options, PyLang) {
113
// See https://nodejs.org/api/readline.html#readline_readline_createinterface_options
114
const completer = Completer(PyLang);
115
const history = options.terminal ? readHistory(options) : [];
116
const readline = (options.mockReadline ?? createInterface)({
117
input: options.input,
118
output: options.output,
119
completer,
120
terminal: options.terminal,
121
history,
122
historySize: options.historySize,
123
tabSize: 4,
124
});
125
// @ts-ignore -- needed for older node.js
126
readline.history = history;
127
return readline;
128
}
129
130
export default async function Repl(options0: Partial<Options>): Promise<void> {
131
const options = replDefaults(options0);
132
const PyLang = createCompiler({
133
console: options.console,
134
});
135
const readline = createReadlineInterface(options, PyLang);
136
const colorize = options.mockReadline
137
? (string, _color?, _bold?) => string
138
: colored;
139
const ps1 = colorize(options.ps1, "blue");
140
const ps2 = colorize(options.ps2, "green");
141
142
// We capture input *during* initialization, so it
143
// doesn't get lost, since initContext is async.
144
let initLines: string[] = [];
145
function duringInit(line: string) {
146
initLines.push(line);
147
}
148
readline.on("line", duringInit);
149
await initContext();
150
readline.off("line", duringInit);
151
152
const buffer: string[] = [];
153
let more: boolean = false;
154
const LINE_CONTINUATION_CHARS = ":\\";
155
let toplevel;
156
var importDirs = getImportDirs();
157
158
/*
159
Python 3.11.0 (main, Nov 29 2022, 20:26:05) [Clang 15.0.3 ([email protected]:ziglang/zig-bootstrap.git 0ce789d0f7a4d89fdc4d9571 on wasi
160
*/
161
if (process.stdin.isTTY) {
162
options.console.log(
163
colorize(
164
`Welcome to PyLang (${(new Date()).toLocaleString()}) [Node.js ${
165
process.version
166
} on ${arch()}]. ${
167
options.jsage ? "\nType dir(jsage) for available functions." : ""
168
}`,
169
"green",
170
true
171
)
172
);
173
}
174
175
function printAST(ast, keepBaselib?: boolean) {
176
const output = new PyLang.OutputStream({
177
omit_baselib: !keepBaselib,
178
write_name: false,
179
private_scope: false,
180
beautify: true,
181
keep_docstrings: true,
182
baselib_plain: keepBaselib
183
? readFileSync(join(libraryPath, "baselib-plain-pretty.js"), "utf-8")
184
: undefined,
185
});
186
ast.print(output);
187
return output.get();
188
}
189
190
async function initContext() {
191
// @ts-ignore
192
global.require = require;
193
194
// and get all the code and name.
195
runInThisContext(printAST(PyLang.parse("(def ():\n yield 1\n)"), true));
196
runInThisContext('var __name__ = "__repl__"; show_js=false;');
197
if (options.jsage) {
198
const BLOCK = true;
199
if (BLOCK) {
200
//const t = new Date().valueOf();
201
// console.log("Initializing jsage...");
202
const jsage = require("@jsage/lib");
203
await jsage.init();
204
//console.log(new Date().valueOf() - t);
205
}
206
runInThisContext("jsage = require('@jsage/lib');");
207
runInThisContext("for(const x in jsage) { global[x] = jsage[x]; }");
208
}
209
}
210
211
function resetBuffer() {
212
buffer.splice(0, buffer.length);
213
}
214
215
function prompt(): void {
216
let leadingWhitespace = "";
217
if (more && buffer.length) {
218
let prev_line = buffer[buffer.length - 1];
219
if (prev_line.trimRight().slice(-1) == ":") {
220
leadingWhitespace = " ";
221
}
222
// Add to leadingWhitespace all the blank space at the beginning of prev_line, if any.
223
const match = prev_line.match(/^\s+/);
224
if (match) {
225
leadingWhitespace += match[0];
226
}
227
}
228
readline.setPrompt(more ? ps2 : ps1);
229
readline.prompt();
230
if (leadingWhitespace) {
231
readline.write(leadingWhitespace);
232
}
233
}
234
235
function runJS(js: string, noPrint: boolean): void {
236
if (runInThisContext("show_js")) {
237
options.console.log(
238
colorize("---------- Compiled JavaScript ---------", "green", true)
239
);
240
options.console.log(js);
241
options.console.log(
242
colorize("---------- Running JavaScript ---------", "green", true)
243
);
244
}
245
let result;
246
try {
247
global.console = options.console;
248
result = runInThisContext(js);
249
} catch (err) {
250
if (err?.stack) {
251
options.console.error(err?.stack);
252
} else {
253
options.console.error(err);
254
}
255
}
256
257
if (!noPrint && result != null && global.ρσ_print != null) {
258
// We just print out the last result using normal Python printing.
259
try {
260
global.ρσ_print(result);
261
} catch (err) {
262
if (err?.stack) {
263
options.console.error(err?.stack);
264
} else {
265
options.console.error(err);
266
}
267
}
268
}
269
}
270
271
// returns true if incomplete
272
function compileAndRun(source: string): boolean {
273
let time: number | undefined = undefined;
274
if (source.startsWith("%time ") || source.startsWith("time ")) {
275
time = 0;
276
source = source.slice(5).trimLeft();
277
}
278
const classes = toplevel?.classes;
279
const scoped_flags = toplevel?.scoped_flags;
280
try {
281
toplevel = PyLang.parse(source, {
282
filename: "<repl>",
283
basedir: process.cwd(),
284
libdir: importPath,
285
import_dirs: importDirs,
286
classes,
287
scoped_flags,
288
jsage: options.jsage,
289
tokens: options.tokens,
290
});
291
} catch (err) {
292
if (err.is_eof && err.line == buffer.length && err.col > 0) {
293
return true;
294
}
295
if (err.message && err.line !== undefined) {
296
options.console.log(err.line + ":" + err.col + ":" + err.message);
297
} else {
298
options.console.log(err.stack || err);
299
}
300
return false;
301
}
302
const output = printAST(toplevel);
303
if (classes) {
304
const exports: { [name: string]: boolean } = {};
305
for (const name in toplevel.exports) {
306
exports[name] = true;
307
}
308
for (const name in classes) {
309
if (!exports[name] && !toplevel.classes[name]) {
310
toplevel.classes[name] = classes[name];
311
}
312
}
313
}
314
const noPrint = source.trimRight().endsWith(";");
315
if (time != null) {
316
time = new Date().valueOf();
317
}
318
runJS(output, noPrint);
319
if (time) {
320
console.log(`Wall time: ${new Date().valueOf() - time}ms`);
321
}
322
return false;
323
}
324
325
// returns true if incomplete
326
function push(line: string): boolean {
327
buffer.push(line);
328
const trimmedLine = line.trimRight();
329
if (
330
trimmedLine &&
331
LINE_CONTINUATION_CHARS.includes(trimmedLine.slice(-1))
332
) {
333
// ends in continuation character after trimming whitespace
334
return true;
335
}
336
const source = buffer.join("\n");
337
if (!source.trim()) {
338
// all whitespace
339
resetBuffer();
340
return false;
341
}
342
const isIncomplete = compileAndRun(source);
343
if (!isIncomplete) {
344
resetBuffer();
345
}
346
return isIncomplete;
347
}
348
349
function readLine(line: string) {
350
if (more) {
351
// We are in a block
352
const lineIsEmpty = !line.trimLeft();
353
if (lineIsEmpty && buffer.length > 0) {
354
// We have an empty lines, so evaluate the block:
355
more = push(line.trimLeft());
356
} else {
357
buffer.push(line);
358
}
359
} else {
360
// Not in a block, evaluate line
361
more = push(line);
362
}
363
prompt();
364
}
365
// Run code we received during initialization.
366
for (const line of initLines) {
367
readLine(line);
368
}
369
370
readline.on("line", readLine);
371
372
readline.on("history", (history) => {
373
// Note -- this only exists in node >15.x.
374
if (options.terminal) {
375
writeHistory(options, history);
376
}
377
});
378
379
readline.on("close", () => {
380
const { history } = readline as any; // deprecated in node 15...
381
if (history) {
382
writeHistory(options, history);
383
}
384
options.console.log();
385
process.exit(0);
386
});
387
388
readline.on("SIGINT", () => {
389
clearLine(options.output, 0);
390
options.console.log("Keyboard Interrupt");
391
resetBuffer();
392
more = false;
393
prompt();
394
});
395
396
readline.on("SIGCONT", prompt);
397
398
prompt();
399
}
400
401