Serenity Operating System
1/*
2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <AK/ByteBuffer.h>
28#include <AK/NonnullOwnPtr.h>
29#include <AK/StringBuilder.h>
30#include <LibCore/ArgsParser.h>
31#include <LibCore/File.h>
32#include <LibJS/AST.h>
33#include <LibJS/Interpreter.h>
34#include <LibJS/Parser.h>
35#include <LibJS/Runtime/Array.h>
36#include <LibJS/Runtime/Date.h>
37#include <LibJS/Runtime/Error.h>
38#include <LibJS/Runtime/Function.h>
39#include <LibJS/Runtime/GlobalObject.h>
40#include <LibJS/Runtime/Object.h>
41#include <LibJS/Runtime/PrimitiveString.h>
42#include <LibJS/Runtime/Shape.h>
43#include <LibJS/Runtime/Value.h>
44#include <LibLine/Editor.h>
45#include <stdio.h>
46
47Vector<String> repl_statements;
48
49class ReplObject : public JS::GlobalObject {
50public:
51 ReplObject();
52 virtual ~ReplObject() override;
53
54private:
55 virtual const char* class_name() const override { return "ReplObject"; }
56 static JS::Value exit_interpreter(JS::Interpreter&);
57 static JS::Value repl_help(JS::Interpreter&);
58 static JS::Value load_file(JS::Interpreter&);
59 static JS::Value save_to_file(JS::Interpreter&);
60};
61
62bool dump_ast = false;
63static OwnPtr<Line::Editor> editor;
64
65String read_next_piece()
66{
67 StringBuilder piece;
68 int level = 0;
69 StringBuilder prompt_builder;
70
71 do {
72 prompt_builder.clear();
73 prompt_builder.append("> ");
74 for (auto i = 0; i < level; ++i)
75 prompt_builder.append(" ");
76
77 String line = editor->get_line(prompt_builder.build());
78 editor->add_to_history(line);
79
80 piece.append(line);
81 auto lexer = JS::Lexer(line);
82
83 for (JS::Token token = lexer.next(); token.type() != JS::TokenType::Eof; token = lexer.next()) {
84 switch (token.type()) {
85 case JS::TokenType::BracketOpen:
86 case JS::TokenType::CurlyOpen:
87 case JS::TokenType::ParenOpen:
88 level++;
89 break;
90 case JS::TokenType::BracketClose:
91 case JS::TokenType::CurlyClose:
92 case JS::TokenType::ParenClose:
93 level--;
94 break;
95 default:
96 break;
97 }
98 }
99 } while (level > 0);
100
101 return piece.to_string();
102}
103
104static void print_value(JS::Value value, HashTable<JS::Object*>& seen_objects);
105
106static void print_array(const JS::Array& array, HashTable<JS::Object*>& seen_objects)
107{
108 fputs("[ ", stdout);
109 for (size_t i = 0; i < array.elements().size(); ++i) {
110 print_value(array.elements()[i], seen_objects);
111 if (i != array.elements().size() - 1)
112 fputs(", ", stdout);
113 }
114 fputs(" ]", stdout);
115}
116
117static void print_object(const JS::Object& object, HashTable<JS::Object*>& seen_objects)
118{
119 fputs("{ ", stdout);
120
121 for (size_t i = 0; i < object.elements().size(); ++i) {
122 if (object.elements()[i].is_empty())
123 continue;
124 printf("\"\033[33;1m%zu\033[0m\": ", i);
125 print_value(object.elements()[i], seen_objects);
126 if (i != object.elements().size() - 1)
127 fputs(", ", stdout);
128 }
129
130 if (!object.elements().is_empty() && object.shape().property_count())
131 fputs(", ", stdout);
132
133 size_t index = 0;
134 for (auto& it : object.shape().property_table()) {
135 printf("\"\033[33;1m%s\033[0m\": ", it.key.characters());
136 print_value(object.get_direct(it.value.offset), seen_objects);
137 if (index != object.shape().property_count() - 1)
138 fputs(", ", stdout);
139 ++index;
140 }
141 fputs(" }", stdout);
142}
143
144static void print_function(const JS::Object& function, HashTable<JS::Object*>&)
145{
146 printf("\033[34;1m[%s]\033[0m", function.class_name());
147}
148
149static void print_date(const JS::Object& date, HashTable<JS::Object*>&)
150{
151 printf("\033[34;1mDate %s\033[0m", static_cast<const JS::Date&>(date).string().characters());
152}
153
154static void print_error(const JS::Object& object, HashTable<JS::Object*>&)
155{
156 auto& error = static_cast<const JS::Error&>(object);
157 printf("\033[34;1m[%s]\033[0m", error.name().characters());
158 if (!error.message().is_empty())
159 printf(": %s", error.message().characters());
160}
161
162void print_value(JS::Value value, HashTable<JS::Object*>& seen_objects)
163{
164 if (value.is_empty()) {
165 printf("\033[34;1m<empty>\033[0m");
166 return;
167 }
168
169 if (value.is_object()) {
170 if (seen_objects.contains(&value.as_object())) {
171 // FIXME: Maybe we should only do this for circular references,
172 // not for all reoccurring objects.
173 printf("<already printed Object %p>", &value.as_object());
174 return;
175 }
176 seen_objects.set(&value.as_object());
177 }
178
179 if (value.is_array())
180 return print_array(static_cast<const JS::Array&>(value.as_object()), seen_objects);
181
182 if (value.is_object()) {
183 auto& object = value.as_object();
184 if (object.is_function())
185 return print_function(object, seen_objects);
186 if (object.is_date())
187 return print_date(object, seen_objects);
188 if (object.is_error())
189 return print_error(object, seen_objects);
190 return print_object(object, seen_objects);
191 }
192
193 if (value.is_string())
194 printf("\033[31;1m");
195 else if (value.is_number())
196 printf("\033[35;1m");
197 else if (value.is_boolean())
198 printf("\033[32;1m");
199 else if (value.is_null())
200 printf("\033[33;1m");
201 else if (value.is_undefined())
202 printf("\033[34;1m");
203 if (value.is_string())
204 putchar('"');
205 printf("%s", value.to_string().characters());
206 if (value.is_string())
207 putchar('"');
208 printf("\033[0m");
209}
210
211static void print(JS::Value value)
212{
213 HashTable<JS::Object*> seen_objects;
214 print_value(value, seen_objects);
215 putchar('\n');
216}
217
218bool file_has_shebang(AK::ByteBuffer file_contents)
219{
220 if (file_contents.size() >= 2 && file_contents[0] == '#' && file_contents[1] == '!')
221 return true;
222 return false;
223}
224
225StringView strip_shebang(AK::ByteBuffer file_contents)
226{
227 size_t i = 0;
228 for (i = 2; i < file_contents.size(); ++i) {
229 if (file_contents[i] == '\n')
230 break;
231 }
232 return StringView((const char*)file_contents.data() + i, file_contents.size() - i);
233}
234
235bool write_to_file(const StringView& path)
236{
237 int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
238 for (size_t i = 0; i < repl_statements.size(); i++) {
239 auto line = repl_statements[i];
240 if (line.length() && i != repl_statements.size() - 1) {
241 ssize_t nwritten = write(fd, line.characters(), line.length());
242 if (nwritten < 0) {
243 close(fd);
244 return false;
245 }
246 }
247 if (i != repl_statements.size() - 1) {
248 char ch = '\n';
249 ssize_t nwritten = write(fd, &ch, 1);
250 if (nwritten != 1) {
251 perror("write");
252 close(fd);
253 return false;
254 }
255 }
256 }
257 close(fd);
258 return true;
259}
260
261ReplObject::ReplObject()
262{
263 put_native_function("exit", exit_interpreter);
264 put_native_function("help", repl_help);
265 put_native_function("load", load_file, 1);
266 put_native_function("save", save_to_file, 1);
267}
268
269ReplObject::~ReplObject()
270{
271}
272JS::Value ReplObject::save_to_file(JS::Interpreter& interpreter)
273{
274 if (!interpreter.argument_count())
275 return JS::Value(false);
276 String save_path = interpreter.argument(0).to_string();
277 StringView path = StringView(save_path.characters());
278 if (write_to_file(path)) {
279 return JS::Value(true);
280 }
281 return JS::Value(false);
282}
283JS::Value ReplObject::exit_interpreter(JS::Interpreter& interpreter)
284{
285 if (!interpreter.argument_count())
286 exit(0);
287 int exit_code = interpreter.argument(0).to_number().as_double();
288 exit(exit_code);
289 return JS::js_undefined();
290}
291JS::Value ReplObject::repl_help(JS::Interpreter& interpreter)
292{
293 StringBuilder help_text;
294 help_text.append("REPL commands:\n");
295 help_text.append(" exit(code): exit the REPL with specified code. Defaults to 0.\n");
296 help_text.append(" help(): display this menu\n");
297 help_text.append(" load(files): Accepts file names as params to load into running session. For example repl.load(\"js/1.js\", \"js/2.js\", \"js/3.js\")\n");
298 String result = help_text.to_string();
299 return js_string(interpreter, result);
300}
301
302JS::Value ReplObject::load_file(JS::Interpreter& interpreter)
303{
304 if (!interpreter.argument_count())
305 return JS::Value(false);
306
307 for (auto& file : interpreter.call_frame().arguments) {
308 String file_name = file.as_string()->string();
309 auto js_file = Core::File::construct(file_name);
310 if (!js_file->open(Core::IODevice::ReadOnly)) {
311 fprintf(stderr, "Failed to open %s: %s\n", file_name.characters(), js_file->error_string());
312 }
313 auto file_contents = js_file->read_all();
314
315 StringView source;
316 if (file_has_shebang(file_contents)) {
317 source = strip_shebang(file_contents);
318 } else {
319 source = file_contents;
320 }
321 auto program = JS::Parser(JS::Lexer(source)).parse_program();
322 if (dump_ast)
323 program->dump(0);
324 interpreter.run(*program);
325 print(interpreter.last_value());
326 }
327 return JS::Value(true);
328}
329
330void repl(JS::Interpreter& interpreter)
331{
332 while (true) {
333 String piece = read_next_piece();
334 if (piece.is_empty())
335 continue;
336 repl_statements.append(piece);
337 auto program = JS::Parser(JS::Lexer(piece)).parse_program();
338 if (dump_ast)
339 program->dump(0);
340
341 interpreter.run(*program);
342 if (interpreter.exception()) {
343 printf("Uncaught exception: ");
344 print(interpreter.exception()->value());
345 interpreter.clear_exception();
346 } else {
347 print(interpreter.last_value());
348 }
349 }
350}
351
352JS::Value assert_impl(JS::Interpreter& interpreter)
353{
354 if (!interpreter.argument_count())
355 return interpreter.throw_exception<JS::Error>("TypeError", "No arguments specified");
356
357 auto assertion_value = interpreter.argument(0);
358 if (!assertion_value.is_boolean())
359 return interpreter.throw_exception<JS::Error>("TypeError", "The first argument is not a boolean");
360
361 if (!assertion_value.to_boolean())
362 return interpreter.throw_exception<JS::Error>("AssertionError", "The assertion failed!");
363
364 return assertion_value;
365}
366
367int main(int argc, char** argv)
368{
369 bool gc_on_every_allocation = false;
370 bool print_last_result = false;
371 bool syntax_highlight = false;
372 bool test_mode = false;
373 const char* script_path = nullptr;
374
375 Core::ArgsParser args_parser;
376 args_parser.add_option(dump_ast, "Dump the AST", "dump-ast", 'A');
377 args_parser.add_option(print_last_result, "Print last result", "print-last-result", 'l');
378 args_parser.add_option(gc_on_every_allocation, "GC on every allocation", "gc-on-every-allocation", 'g');
379 args_parser.add_option(syntax_highlight, "Enable live syntax highlighting", "syntax-highlight", 's');
380 args_parser.add_option(test_mode, "Run the interpretter with added functionality for the test harness", "test-mode", 't');
381 args_parser.add_positional_argument(script_path, "Path to script file", "script", Core::ArgsParser::Required::No);
382 args_parser.parse(argc, argv);
383
384 if (script_path == nullptr) {
385 auto interpreter = JS::Interpreter::create<ReplObject>();
386 interpreter->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);
387 if (test_mode) {
388 interpreter->global_object().put_native_function("assert", assert_impl);
389 }
390
391 editor = make<Line::Editor>();
392 editor->initialize();
393 if (syntax_highlight)
394 editor->on_display_refresh = [](Line::Editor& editor) {
395 editor.strip_styles();
396 StringBuilder builder;
397 builder.append({ editor.buffer().data(), editor.buffer().size() });
398 // FIXME: The lexer returns weird position information without this
399 builder.append(" ");
400 String str = builder.build();
401
402 JS::Lexer lexer(str, false);
403 for (JS::Token token = lexer.next(); token.type() != JS::TokenType::Eof; token = lexer.next()) {
404 auto length = token.value().length();
405 auto start = token.line_column() - 2;
406 auto end = start + length;
407
408 switch (token.type()) {
409 case JS::TokenType::Invalid:
410 case JS::TokenType::Eof:
411 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Red), Line::Style::Underline });
412 break;
413 case JS::TokenType::NumericLiteral:
414 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Magenta) });
415 break;
416 case JS::TokenType::StringLiteral:
417 case JS::TokenType::RegexLiteral:
418 case JS::TokenType::UnterminatedStringLiteral:
419 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Red) });
420 break;
421 case JS::TokenType::BracketClose:
422 case JS::TokenType::BracketOpen:
423 case JS::TokenType::Caret:
424 case JS::TokenType::Comma:
425 case JS::TokenType::CurlyClose:
426 case JS::TokenType::CurlyOpen:
427 case JS::TokenType::ParenClose:
428 case JS::TokenType::ParenOpen:
429 case JS::TokenType::Semicolon:
430 case JS::TokenType::Period:
431 break;
432 case JS::TokenType::Ampersand:
433 case JS::TokenType::AmpersandEquals:
434 case JS::TokenType::Asterisk:
435 case JS::TokenType::AsteriskAsteriskEquals:
436 case JS::TokenType::AsteriskEquals:
437 case JS::TokenType::DoubleAmpersand:
438 case JS::TokenType::DoubleAsterisk:
439 case JS::TokenType::DoublePipe:
440 case JS::TokenType::DoubleQuestionMark:
441 case JS::TokenType::Equals:
442 case JS::TokenType::EqualsEquals:
443 case JS::TokenType::EqualsEqualsEquals:
444 case JS::TokenType::ExclamationMark:
445 case JS::TokenType::ExclamationMarkEquals:
446 case JS::TokenType::ExclamationMarkEqualsEquals:
447 case JS::TokenType::GreaterThan:
448 case JS::TokenType::GreaterThanEquals:
449 case JS::TokenType::LessThan:
450 case JS::TokenType::LessThanEquals:
451 case JS::TokenType::Minus:
452 case JS::TokenType::MinusEquals:
453 case JS::TokenType::MinusMinus:
454 case JS::TokenType::Percent:
455 case JS::TokenType::PercentEquals:
456 case JS::TokenType::Pipe:
457 case JS::TokenType::PipeEquals:
458 case JS::TokenType::Plus:
459 case JS::TokenType::PlusEquals:
460 case JS::TokenType::PlusPlus:
461 case JS::TokenType::QuestionMark:
462 case JS::TokenType::QuestionMarkPeriod:
463 case JS::TokenType::ShiftLeft:
464 case JS::TokenType::ShiftLeftEquals:
465 case JS::TokenType::ShiftRight:
466 case JS::TokenType::ShiftRightEquals:
467 case JS::TokenType::Slash:
468 case JS::TokenType::SlashEquals:
469 case JS::TokenType::Tilde:
470 case JS::TokenType::UnsignedShiftRight:
471 case JS::TokenType::UnsignedShiftRightEquals:
472 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Magenta) });
473 break;
474 case JS::TokenType::NullLiteral:
475 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Yellow), Line::Style::Bold });
476 break;
477 case JS::TokenType::BoolLiteral:
478 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Green), Line::Style::Bold });
479 break;
480 case JS::TokenType::Class:
481 case JS::TokenType::Const:
482 case JS::TokenType::Delete:
483 case JS::TokenType::Function:
484 case JS::TokenType::In:
485 case JS::TokenType::Instanceof:
486 case JS::TokenType::Interface:
487 case JS::TokenType::Let:
488 case JS::TokenType::New:
489 case JS::TokenType::Typeof:
490 case JS::TokenType::Var:
491 case JS::TokenType::Void:
492 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Blue), Line::Style::Bold });
493 break;
494 case JS::TokenType::Await:
495 case JS::TokenType::Catch:
496 case JS::TokenType::Do:
497 case JS::TokenType::Else:
498 case JS::TokenType::Finally:
499 case JS::TokenType::For:
500 case JS::TokenType::If:
501 case JS::TokenType::Return:
502 case JS::TokenType::Try:
503 case JS::TokenType::While:
504 case JS::TokenType::Yield:
505 editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::Color::Cyan), Line::Style::Italic });
506 break;
507 case JS::TokenType::Identifier:
508 default:
509 break;
510 }
511 }
512 };
513 repl(*interpreter);
514 } else {
515 auto interpreter = JS::Interpreter::create<JS::GlobalObject>();
516 interpreter->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);
517 if (test_mode) {
518 interpreter->global_object().put_native_function("assert", assert_impl);
519 }
520
521 auto file = Core::File::construct(script_path);
522 if (!file->open(Core::IODevice::ReadOnly)) {
523 fprintf(stderr, "Failed to open %s: %s\n", script_path, file->error_string());
524 return 1;
525 }
526 auto file_contents = file->read_all();
527
528 StringView source;
529 if (file_has_shebang(file_contents)) {
530 source = strip_shebang(file_contents);
531 } else {
532 source = file_contents;
533 }
534 auto program = JS::Parser(JS::Lexer(source)).parse_program();
535
536 if (dump_ast)
537 program->dump(0);
538
539 auto result = interpreter->run(*program);
540
541 if (interpreter->exception()) {
542 printf("Uncaught exception: ");
543 print(interpreter->exception()->value());
544 interpreter->clear_exception();
545 return 1;
546 }
547 if (print_last_result)
548 print(result);
549 }
550
551 return 0;
552}