Serenity Operating System
at hosted 552 lines 21 kB view raw
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}