/* * Copyright (c) 2020-2022 Thakee Nathees * Copyright (c) 2021-2022 Pocketlang Contributors * Distributed Under The MIT License */ #include "pk_core.h" #include #include #include #include #include "pk_debug.h" #include "pk_utils.h" #include "pk_vm.h" // Returns the docstring of the function, which is a static const char* defined // just above the function by the DEF() macro below. #define DOCSTRING(fn) _pk_doc_##fn // A macro to declare a function, with docstring, which is defined as // _pk_doc_ = docstring; That'll used to generate function help text. #define DEF(fn, docstring) \ static const char* DOCSTRING(fn) = docstring; \ static void fn(PKVM* vm) // A convenient macro to get the nth (1 based) argument of the current // function. #define ARG(n) (vm->fiber->ret[n]) // Evaluates to the current function's argument count. #define ARGC ((int)(vm->fiber->sp - vm->fiber->ret) - 1) // Set return value for the current native function and return. #define RET(value) \ do { \ *(vm->fiber->ret) = value; \ return; \ } while (false) #define RET_ERR(err) \ do { \ VM_SET_ERROR(vm, err); \ RET(VAR_NULL); \ } while(false) /*****************************************************************************/ /* VALIDATORS */ /*****************************************************************************/ // Evaluated to true of the [num] is in byte range. #define IS_NUM_BYTE(num) ((CHAR_MIN <= (num)) && ((num) <= CHAR_MAX)) // Check if [var] is a numeric value (bool/number) and set [value]. static inline bool isNumeric(Var var, double* value) { if (IS_NUM(var)) { *value = AS_NUM(var); return true; } if (IS_BOOL(var)) { *value = AS_BOOL(var); return true; } return false; } // Check if [var] is a numeric value (bool/number) and set [value]. static inline bool isInteger(Var var, int64_t* value) { double number; if (isNumeric(var, &number)) { // TODO: check if the number is larger for a 64 bit integer. if (floor(number) == number) { ASSERT(INT64_MIN <= number && number <= INT64_MAX, "TODO: Large numbers haven't handled yet. Please report!"); *value = (int64_t)(number); return true; } } return false; } // Check if [var] is bool/number. If not, it'll set error and return false. static inline bool validateNumeric(PKVM* vm, Var var, double* value, const char* name) { if (isNumeric(var, value)) return true; VM_SET_ERROR(vm, stringFormat(vm, "$ must be a numeric value.", name)); return false; } // Check if [var] is 32 bit integer. If not, it'll set error and return false. static inline bool validateInteger(PKVM* vm, Var var, int64_t* value, const char* name) { if (isInteger(var, value)) return true; VM_SET_ERROR(vm, stringFormat(vm, "$ must be a whole number.", name)); return false; } // Index is could be larger than 32 bit integer, but the size in pocketlang // limited to 32 unsigned bit integer static inline bool validateIndex(PKVM* vm, int64_t index, uint32_t size, const char* container) { if (index < 0 || size <= index) { VM_SET_ERROR(vm, stringFormat(vm, "$ index out of bound.", container)); return false; } return true; } // Check if the [condition] is true. If not, it'll set an error and return // false. static inline bool validateCond(PKVM* vm, bool condition, const char* err) { if (!condition) { VM_SET_ERROR(vm, newString(vm, err)); return false; } return true; } // Check if [var] is string for argument at [arg]. If not set error and // return false. #define VALIDATE_ARG_OBJ(m_class, m_type, m_name) \ static bool validateArg##m_class(PKVM* vm, int arg, m_class** value) { \ Var var = ARG(arg); \ ASSERT(arg > 0 && arg <= ARGC, OOPS); \ if (!IS_OBJ(var) || AS_OBJ(var)->type != m_type) { \ char buff[12]; sprintf(buff, "%d", arg); \ VM_SET_ERROR(vm, stringFormat(vm, "Expected a " m_name \ " at argument $.", buff, false)); \ return false; \ } \ *value = (m_class*)AS_OBJ(var); \ return true; \ } VALIDATE_ARG_OBJ(String, OBJ_STRING, "string") VALIDATE_ARG_OBJ(List, OBJ_LIST, "list") VALIDATE_ARG_OBJ(Map, OBJ_MAP, "map") VALIDATE_ARG_OBJ(Closure, OBJ_CLOSURE, "closure") VALIDATE_ARG_OBJ(Fiber, OBJ_FIBER, "fiber") VALIDATE_ARG_OBJ(Class, OBJ_CLASS, "class") /*****************************************************************************/ /* SHARED FUNCTIONS */ /*****************************************************************************/ static void initializeBuiltinFunctions(PKVM* vm); static void initializeCoreModules(PKVM* vm); static void initializePrimitiveClasses(PKVM* vm); void initializeCore(PKVM* vm) { initializeBuiltinFunctions(vm); initializeCoreModules(vm); initializePrimitiveClasses(vm); } /*****************************************************************************/ /* INTERNAL FUNCTIONS */ /*****************************************************************************/ // Returns the string value of the variable, a wrapper of toString() function // but for instances it'll try to calll "_to_string" function and on error // it'll return NULL. static inline String* _varToString(PKVM* vm, Var self) { if (IS_OBJ_TYPE(self, OBJ_INST)) { // The closure is retrieved from [self] thus, it doesn't need to be push // on the VM's temp references (since [self] should already be protected // from GC). Closure* closure = NULL; String* name = newString(vm, LITS__str); // TODO: static vm string?. vmPushTempRef(vm, &name->_super); // method. bool has_method = hasMethod(vm, self, name, &closure); vmPopTempRef(vm); // method. if (has_method) { Var ret = VAR_NULL; PkResult result = vmCallMethod(vm, self, closure, 0, NULL, &ret); if (result != PK_RESULT_SUCCESS) return NULL; if (!IS_OBJ_TYPE(ret, OBJ_STRING)) { VM_SET_ERROR(vm, newString(vm, "method " LITS__str " returned " "non-string type.")); return NULL; } return (String*)AS_OBJ(ret); } // If we reached here, it doesn't have a to string override. just // "fall throught" and call 'toString()' bellow. } return toString(vm, self); } // Calls a unary operator overload method. If the method does not exists it'll // return false, otherwise it'll call the method and return true. If any error // occures it'll set an error. static inline bool _callUnaryOpMethod(PKVM* vm, Var self, const char* method_name, Var* ret) { Closure* closure = NULL; String* name = newString(vm, method_name); vmPushTempRef(vm, &name->_super); // method. bool has_method = hasMethod(vm, self, name, &closure); vmPopTempRef(vm); // method. if (!has_method) return false; vmCallMethod(vm, self, closure, 0, NULL, ret); return true; } // Calls a binary operator overload method. If the method does not exists it'll // return false, otherwise it'll call the method and return true. If any error // occures it'll set an error. static inline bool _callBinaryOpMethod(PKVM* vm, Var self, Var other, const char* method_name, Var* ret) { Closure* closure = NULL; String* name = newString(vm, method_name); vmPushTempRef(vm, &name->_super); // method. bool has_method = hasMethod(vm, self, name, &closure); vmPopTempRef(vm); // method. if (!has_method) return false; vmCallMethod(vm, self, closure, 1, &other, ret); return true; } /*****************************************************************************/ /* CORE BUILTIN FUNCTIONS */ /*****************************************************************************/ DEF(coreTypeName, "type_name(value:var) -> string\n" "Returns the type name of the of the value.") { RET(VAR_OBJ(newString(vm, varTypeName(ARG(1))))); } DEF(coreHelp, "help([fn:Closure]) -> null\n" "This will write an error message to stdout and return null.") { int argc = ARGC; if (argc != 0 && argc != 1) { RET_ERR(newString(vm, "Invalid argument count.")); } if (argc == 0) { // If there ins't an io function callback, we're done. if (vm->config.stdout_write == NULL) RET(VAR_NULL); vm->config.stdout_write(vm, "TODO: print help here\n"); } else if (argc == 1) { // TODO: Extend help() to work with modules and classes. // Add docstring (like python) to support it in pocketlang. Closure* closure; if (!validateArgClosure(vm, 1, &closure)) return; // If there ins't an io function callback, we're done. if (vm->config.stdout_write == NULL) RET(VAR_NULL); if (closure->fn->docstring != NULL) { vm->config.stdout_write(vm, closure->fn->docstring); vm->config.stdout_write(vm, "\n\n"); } else { vm->config.stdout_write(vm, "function '"); vm->config.stdout_write(vm, closure->fn->name); vm->config.stdout_write(vm, "()' doesn't have a docstring.\n"); } } } DEF(coreAssert, "assert(condition:bool [, msg:string]) -> void\n" "If the condition is false it'll terminate the current fiber with the " "optional error message") { int argc = ARGC; if (argc != 1 && argc != 2) { RET_ERR(newString(vm, "Invalid argument count.")); } if (!toBool(ARG(1))) { String* msg = NULL; if (argc == 2) { if (AS_OBJ(ARG(2))->type != OBJ_STRING) { msg = _varToString(vm, ARG(2)); if (msg == NULL) return; //< Error at _to_string override. } else { msg = (String*)AS_OBJ(ARG(2)); } vmPushTempRef(vm, &msg->_super); // msg. VM_SET_ERROR(vm, stringFormat(vm, "Assertion failed: '@'.", msg)); vmPopTempRef(vm); // msg. } else { VM_SET_ERROR(vm, newString(vm, "Assertion failed.")); } } } DEF(coreBin, "bin(value:num) -> string\n" "Returns as a binary value string with '0b' prefix.") { int64_t value; if (!validateInteger(vm, ARG(1), &value, "Argument 1")) return; char buff[STR_BIN_BUFF_SIZE]; bool negative = (value < 0) ? true : false; if (negative) value = -value; char* ptr = buff + STR_BIN_BUFF_SIZE - 1; *ptr-- = '\0'; // NULL byte at the end of the string. if (value != 0) { while (value > 0) { *ptr-- = '0' + (value & 1); value >>= 1; } } else { *ptr-- = '0'; } *ptr-- = 'b'; *ptr-- = '0'; if (negative) *ptr-- = '-'; uint32_t length = (uint32_t)((buff + STR_BIN_BUFF_SIZE - 1) - (ptr + 1)); RET(VAR_OBJ(newStringLength(vm, ptr + 1, length))); } DEF(coreHex, "hex(value:num) -> string\n" "Returns as a hexadecimal value string with '0x' prefix.") { int64_t value; if (!validateInteger(vm, ARG(1), &value, "Argument 1")) return; char buff[STR_HEX_BUFF_SIZE]; char* ptr = buff; if (value < 0) *ptr++ = '-'; *ptr++ = '0'; *ptr++ = 'x'; if (value > UINT32_MAX || value < -(int64_t)(UINT32_MAX)) { VM_SET_ERROR(vm, newString(vm, "Integer is too large.")); RET(VAR_NULL); } // TODO: sprintf limits only to 8 character hex value, we need to do it // outself for a maximum of 16 character long (see bin() for reference). uint32_t _x = (uint32_t)((value < 0) ? -value : value); int length = sprintf(ptr, "%x", _x); RET(VAR_OBJ(newStringLength(vm, buff, (uint32_t)((ptr + length) - (char*)(buff))))); } DEF(coreYield, "yield([value]) -> var\n" "Return the current function with the yield [value] to current running " "fiber. If the fiber is resumed, it'll run from the next statement of the " "yield() call. If the fiber resumed with with a value, the return value of " "the yield() would be that value otherwise null.") { int argc = ARGC; if (argc > 1) { // yield() or yield(val). RET_ERR(newString(vm, "Invalid argument count.")); } vmYieldFiber(vm, (argc == 1) ? &ARG(1) : NULL); } DEF(coreToString, "str(value:var) -> string\n" "Returns the string representation of the value.") { String* str = _varToString(vm, ARG(1)); if (str == NULL) RET(VAR_NULL); RET(VAR_OBJ(str)); } DEF(coreChr, "chr(value:num) -> string\n" "Returns the ASCII string value of the integer argument.") { int64_t num; if (!validateInteger(vm, ARG(1), &num, "Argument 1")) return; if (!IS_NUM_BYTE(num)) { RET_ERR(newString(vm, "The number is not in a byte range.")); } char c = (char)num; RET(VAR_OBJ(newStringLength(vm, &c, 1))); } DEF(coreOrd, "ord(value:string) -> num\n" "Returns integer value of the given ASCII character.") { String* c; if (!validateArgString(vm, 1, &c)) return; if (c->length != 1) { RET_ERR(newString(vm, "Expected a string of length 1.")); } else { RET(VAR_NUM((double)c->data[0])); } } DEF(corePrint, "print(...) -> void\n" "Write each argument as space seperated, to the stdout and ends with a " "newline.") { // If the host application doesn't provide any write function, discard the // output. if (vm->config.stdout_write == NULL) return; for (int i = 1; i <= ARGC; i++) { if (i != 1) vm->config.stdout_write(vm, " "); String* str = _varToString(vm, ARG(i)); if (str == NULL) RET(VAR_NULL); vm->config.stdout_write(vm, str->data); } vm->config.stdout_write(vm, "\n"); } DEF(coreInput, "input([msg:var]) -> string\n" "Read a line from stdin and returns it without the line ending. Accepting " "an optional argument [msg] and prints it before reading.") { int argc = ARGC; if (argc != 1 && argc != 2) { RET_ERR(newString(vm, "Invalid argument count.")); } // If the host application doesn't provide any write function, return. if (vm->config.stdin_read == NULL) return; if (argc == 1) { String* str = _varToString(vm, ARG(1)); if (str == NULL) RET(VAR_NULL); vm->config.stdout_write(vm, str->data); } PkStringPtr result = vm->config.stdin_read(vm); String* line = newString(vm, result.string); if (result.on_done) result.on_done(vm, result); RET(VAR_OBJ(line)); } DEF(coreExit, "exit([value:num]) -> null\n" "Exit the process with an optional exit code provided by the argument " "[value]. The default exit code is would be 0.") { int argc = ARGC; if (argc > 1) { // exit() or exit(val). RET_ERR(newString(vm, "Invalid argument count.")); } int64_t value = 0; if (argc == 1) { if (!validateInteger(vm, ARG(1), &value, "Argument 1")) return; } // TODO: this actually needs to be the VM fiber being set to null though. exit((int)value); } // String functions. // ----------------- DEF(coreStrSub, "str_sub(str:string, pos:num, len:num) -> string\n" "Returns a substring from a given string supplied. In addition, " "the position and length of the substring are provided when this " "function is called. For example: `str_sub(str, pos, len)`.") { String* str; int64_t pos, len; if (!validateArgString(vm, 1, &str)) return; if (!validateInteger(vm, ARG(2), &pos, "Argument 2")) return; if (!validateInteger(vm, ARG(3), &len, "Argument 3")) return; if (pos < 0 || str->length < pos) RET_ERR(newString(vm, "Index out of range.")); if (str->length < pos + len) RET_ERR(newString(vm, "Substring length exceeded the limit.")); // Edge case, empty string. if (len == 0) RET(VAR_OBJ(newStringLength(vm, "", 0))); RET(VAR_OBJ(newStringLength(vm, str->data + pos, (uint32_t)len))); } // List functions. // --------------- DEF(coreListAppend, "list_append(self:List, value:var) -> List\n" "Append the [value] to the list [self] and return the list.") { List* list; if (!validateArgList(vm, 1, &list)) return; Var elem = ARG(2); listAppend(vm, list, elem); RET(VAR_OBJ(list)); } // TODO: currently it takes one argument (to test string interpolation). // Add join delimeter as an optional argument. DEF(coreListJoin, "list_join(self:List) -> String\n" "Concatinate the elements of the list and return as a string.") { List* list; if (!validateArgList(vm, 1, &list)) return; pkByteBuffer buff; pkByteBufferInit(&buff); for (uint32_t i = 0; i < list->elements.count; i++) { String* str = _varToString(vm, list->elements.data[i]); if (str == NULL) RET(VAR_NULL); vmPushTempRef(vm, &str->_super); // elem pkByteBufferAddString(&buff, vm, str->data, str->length); vmPopTempRef(vm); // elem } String* str = newStringLength(vm, (const char*)buff.data, buff.count); pkByteBufferClear(&buff, vm); RET(VAR_OBJ(str)); } // Map functions. // -------------- DEF(coreMapRemove, "map_remove(self:map, key:var) -> var\n" "Remove the [key] from the map [self] and return it's value if the key " "exists, otherwise it'll return null.") { Map* map; if (!validateArgMap(vm, 1, &map)) return; Var key = ARG(2); RET(mapRemoveKey(vm, map, key)); } static void initializeBuiltinFN(PKVM* vm, Closure** bfn, const char* name, int length, int arity, pkNativeFn ptr, const char* docstring) { Function* fn = newFunction(vm, name, length, NULL, true, docstring, NULL); fn->arity = arity; fn->native = ptr; vmPushTempRef(vm, &fn->_super); // fn. *bfn = newClosure(vm, fn); vmPopTempRef(vm); // fn. } static void initializeBuiltinFunctions(PKVM* vm) { #define INITIALIZE_BUILTIN_FN(name, fn, argc) \ initializeBuiltinFN(vm, &vm->builtins_funcs[vm->builtins_count++], name, \ (int)strlen(name), argc, fn, DOCSTRING(fn)); // General functions. INITIALIZE_BUILTIN_FN("type_name", coreTypeName, 1); INITIALIZE_BUILTIN_FN("help", coreHelp, -1); INITIALIZE_BUILTIN_FN("assert", coreAssert, -1); INITIALIZE_BUILTIN_FN("bin", coreBin, 1); INITIALIZE_BUILTIN_FN("hex", coreHex, 1); INITIALIZE_BUILTIN_FN("yield", coreYield, -1); INITIALIZE_BUILTIN_FN("str", coreToString, 1); INITIALIZE_BUILTIN_FN("chr", coreChr, 1); INITIALIZE_BUILTIN_FN("ord", coreOrd, 1); INITIALIZE_BUILTIN_FN("print", corePrint, -1); INITIALIZE_BUILTIN_FN("input", coreInput, -1); INITIALIZE_BUILTIN_FN("exit", coreExit, -1); // FIXME: // move this functions as methods. and make "append()" a builtin. // String functions. INITIALIZE_BUILTIN_FN("str_sub", coreStrSub, 3); // List functions. INITIALIZE_BUILTIN_FN("list_append", coreListAppend, 2); INITIALIZE_BUILTIN_FN("list_join", coreListJoin, 1); // Map functions. INITIALIZE_BUILTIN_FN("map_remove", coreMapRemove, 2); #undef INITIALIZE_BUILTIN_FN } /*****************************************************************************/ /* CORE MODULE METHODS */ /*****************************************************************************/ // Create a module and add it to the vm's core modules, returns the module. Module* newModuleInternal(PKVM* vm, const char* name) { String* _name = newString(vm, name); vmPushTempRef(vm, &_name->_super); // _name // Check if any module with the same name already exists and assert to the // hosting application. if (vmGetModule(vm, _name) != NULL) { ASSERT(false, stringFormat(vm, "A module named '$' already exists", name)->data); } Module* module = newModule(vm); module->name = _name; module->initialized = true; vmPopTempRef(vm); // _name return module; } // An internal function to add a function to the given [module]. void moduleAddFunctionInternal(PKVM* vm, Module* module, const char* name, pkNativeFn fptr, int arity, const char* docstring) { Function* fn = newFunction(vm, name, (int)strlen(name), module, true, docstring, NULL); fn->native = fptr; fn->arity = arity; vmPushTempRef(vm, &fn->_super); // fn. Closure* closure = newClosure(vm, fn); moduleAddGlobal(vm, module, name, (uint32_t)strlen(name), VAR_OBJ(closure)); vmPopTempRef(vm); // fn. } // 'lang' library methods. // ----------------------- DEF(stdLangClock, "clock() -> num\n" "Returns the number of seconds since the application started") { RET(VAR_NUM((double)clock() / CLOCKS_PER_SEC)); } DEF(stdLangGC, "gc() -> num\n" "Trigger garbage collection and return the amount of bytes cleaned.") { size_t bytes_before = vm->bytes_allocated; vmCollectGarbage(vm); size_t garbage = bytes_before - vm->bytes_allocated; RET(VAR_NUM((double)garbage)); } DEF(stdLangDisas, "disas(fn:Closure) -> String\n" "Returns the disassembled opcode of the function [fn].") { // TODO: support dissasemble class constructors and module main body. Closure* closure; if (!validateArgClosure(vm, 1, &closure)) return; if (!validateCond(vm, !closure->fn->is_native, "Cannot disassemble native functions.")) return; dumpFunctionCode(vm, closure->fn); } #ifdef DEBUG DEF(stdLangDebugBreak, "debug_break() -> null\n" "A debug function for development (will be removed).") { DEBUG_BREAK(); } #endif DEF(stdLangWrite, "write(...) -> null\n" "Write function, just like print function but it wont put space between" "args and write a new line at the end.") { // If the host application doesn't provide any write function, discard the // output. if (vm->config.stdout_write == NULL) return; String* str; //< Will be cleaned by garbage collector; for (int i = 1; i <= ARGC; i++) { Var arg = ARG(i); // If it's already a string don't allocate a new string instead use it. if (IS_OBJ_TYPE(arg, OBJ_STRING)) { str = (String*)AS_OBJ(arg); } else { str = _varToString(vm, ARG(1)); if (str == NULL) RET(VAR_NULL); } vm->config.stdout_write(vm, str->data); } } static void initializeCoreModules(PKVM* vm) { #define MODULE_ADD_FN(module, name, fn, argc) \ moduleAddFunctionInternal(vm, module, name, fn, argc, DOCSTRING(fn)) #define NEW_MODULE(module, name_string) \ Module* module = newModuleInternal(vm, name_string); \ vmPushTempRef(vm, &module->_super); /* module */ \ vmRegisterModule(vm, module, module->name); \ vmPopTempRef(vm) /* module */ NEW_MODULE(lang, "lang"); MODULE_ADD_FN(lang, "clock", stdLangClock, 0); MODULE_ADD_FN(lang, "gc", stdLangGC, 0); MODULE_ADD_FN(lang, "disas", stdLangDisas, 1); MODULE_ADD_FN(lang, "write", stdLangWrite, -1); #ifdef DEBUG MODULE_ADD_FN(lang, "debug_break", stdLangDebugBreak, 0); #endif #undef MODULE_ADD_FN #undef NEW_MODULE } /*****************************************************************************/ /* BUILTIN CLASS CONSTRUCTORS */ /*****************************************************************************/ static void _ctorNull(PKVM* vm) { RET(VAR_NULL); } static void _ctorBool(PKVM* vm) { RET(VAR_BOOL(toBool(ARG(1)))); } static void _ctorNumber(PKVM* vm) { double value; if (!validateNumeric(vm, ARG(1), &value, "Argument 1")) return; RET(VAR_NUM(value)); } static void _ctorString(PKVM* vm) { if (!pkCheckArgcRange(vm, ARGC, 0, 1)) return; if (ARGC == 0) { RET(VAR_OBJ(newStringLength(vm, NULL, 0))); return; } String* str = _varToString(vm, ARG(1)); if (str == NULL) RET(VAR_NULL); RET(VAR_OBJ(str)); } static void _ctorList(PKVM* vm) { List* list = newList(vm, ARGC); vmPushTempRef(vm, &list->_super); // list. for (int i = 0; i < ARGC; i++) { listAppend(vm, list, ARG(i + 1)); } vmPopTempRef(vm); // list. RET(VAR_OBJ(list)); } static void _ctorMap(PKVM* vm) { RET(VAR_OBJ(newMap(vm))); } static void _ctorRange(PKVM* vm) { double from, to; if (!validateNumeric(vm, ARG(1), &from, "Argument 1")) return; if (!validateNumeric(vm, ARG(2), &to, "Argument 2")) return; RET(VAR_OBJ(newRange(vm, from, to))); } static void _ctorFiber(PKVM* vm) { Closure* closure; if (!validateArgClosure(vm, 1, &closure)) return; RET(VAR_OBJ(newFiber(vm, closure))); } /*****************************************************************************/ /* BUILTIN CLASS METHODS */ /*****************************************************************************/ #define SELF (vm->fiber->self) DEF(_listAppend, "List.append(value:var) -> List\n" "Append the [value] to the list and return the list.") { ASSERT(IS_OBJ_TYPE(SELF, OBJ_LIST), OOPS); listAppend(vm, ((List*)AS_OBJ(SELF)), ARG(1)); RET(SELF); } DEF(_fiberRun, "Fiber.run(...) -> var\n" "Runs the fiber's function with the provided arguments and returns it's " "return value or the yielded value if it's yielded.") { ASSERT(IS_OBJ_TYPE(SELF, OBJ_FIBER), OOPS); Fiber* self = (Fiber*)AS_OBJ(SELF); // Switch fiber and start execution. New fibers are marked as running in // either it's stats running with vmRunFiber() or here -- inserting a // fiber over a running (callee) fiber. if (vmPrepareFiber(vm, self, ARGC, &ARG(1))) { self->caller = vm->fiber; vm->fiber = self; self->state = FIBER_RUNNING; } } DEF(_fiberResume, "Fiber.resume() -> var\n" "Resumes a yielded function from a previous call of fiber_run() function. " "Return it's return value or the yielded value if it's yielded.") { ASSERT(IS_OBJ_TYPE(SELF, OBJ_FIBER), OOPS); Fiber* self = (Fiber*)AS_OBJ(SELF); if (!pkCheckArgcRange(vm, ARGC, 0, 1)) return; Var value = (ARGC == 1) ? ARG(1) : VAR_NULL; // Switch fiber and resume execution. if (vmSwitchFiber(vm, self, &value)) { self->state = FIBER_RUNNING; } } #undef SELF /*****************************************************************************/ /* BUILTIN CLASS INITIALIZATION */ /*****************************************************************************/ static void initializePrimitiveClasses(PKVM* vm) { for (int i = 0; i < PK_INSTANCE; i++) { Class* super = NULL; if (i != 0) super = vm->builtin_classes[PK_OBJECT]; const char* name = getPkVarTypeName((PkVarType)i); Class* cls = newClass(vm, name, (int)strlen(name), super, NULL, NULL, NULL); vm->builtin_classes[i] = cls; cls->class_of = (PkVarType)i; } #define ADD_CTOR(type, name, ptr, arity_) \ do { \ Function* fn = newFunction(vm, name, (int)strlen(name), \ NULL, true, NULL, NULL); \ fn->native = ptr; \ fn->arity = arity_; \ vmPushTempRef(vm, &fn->_super); /* fn. */ \ vm->builtin_classes[type]->ctor = newClosure(vm, fn); \ vmPopTempRef(vm); /* fn. */ \ } while (false) ADD_CTOR(PK_NULL, "@ctorNull", _ctorNull, 0); ADD_CTOR(PK_BOOL, "@ctorBool", _ctorBool, 1); ADD_CTOR(PK_NUMBER, "@ctorNumber", _ctorNumber, 1); ADD_CTOR(PK_STRING, "@ctorString", _ctorString, -1); ADD_CTOR(PK_LIST, "@ctorList", _ctorList, -1); ADD_CTOR(PK_MAP, "@ctorMap", _ctorMap, 0); ADD_CTOR(PK_FIBER, "@ctorFiber", _ctorFiber, 1); #undef ADD_CTOR #define ADD_METHOD(type, name, ptr, arity_) \ do { \ Function* fn = newFunction(vm, name, (int)strlen(name), \ NULL, true, DOCSTRING(ptr), NULL); \ fn->native = ptr; \ fn->arity = arity_; \ vmPushTempRef(vm, &fn->_super); /* fn. */ \ pkClosureBufferWrite(&vm->builtin_classes[type]->methods, \ vm, newClosure(vm, fn)); \ vmPopTempRef(vm); /* fn. */ \ } while (false) ADD_METHOD(PK_LIST, "append", _listAppend, 1); ADD_METHOD(PK_FIBER, "run", _fiberRun, -1); ADD_METHOD(PK_FIBER, "resume", _fiberResume, -1); #undef ADD_METHOD } #undef IS_NUM_BYTE #undef DOCSTRING #undef DEF /*****************************************************************************/ /* OPERATORS */ /*****************************************************************************/ Var preConstructSelf(PKVM* vm, Class* cls) { #define NO_INSTANCE(type_name) \ VM_SET_ERROR(vm, newString(vm, \ "Class '" type_name "' cannot be instanciated.")) switch (cls->class_of) { case PK_OBJECT: NO_INSTANCE("Object"); return VAR_NULL; case PK_NULL: case PK_BOOL: case PK_NUMBER: case PK_STRING: case PK_LIST: case PK_MAP: case PK_RANGE: return VAR_NULL; // Constructor will override the null. case PK_MODULE: NO_INSTANCE("Module"); return VAR_NULL; case PK_CLOSURE: NO_INSTANCE("Closure"); return VAR_NULL; case PK_FIBER: return VAR_NULL; case PK_CLASS: NO_INSTANCE("Class"); return VAR_NULL; case PK_INSTANCE: return VAR_OBJ(newInstance(vm, cls)); } UNREACHABLE(); return VAR_NULL; } Class* getClass(PKVM* vm, Var instance) { PkVarType type = getVarType(instance); if (0 <= type && type < PK_INSTANCE) { return vm->builtin_classes[type]; } ASSERT(IS_OBJ_TYPE(instance, OBJ_INST), OOPS); Instance* inst = (Instance*)AS_OBJ(instance); return inst->cls; } // Returns a method on a class (it'll walk up the inheritance tree to search // and if the method not found, it'll return NULL. static inline Closure* clsGetMethod(Class* cls, String* name) { Class* cls_ = cls; do { for (int i = 0; i < (int)cls_->methods.count; i++) { Closure* method_ = cls_->methods.data[i]; if (IS_CSTR_EQ(name, method_->fn->name, name->length)) { return method_; } } cls_ = cls_->super_class; } while (cls_ != NULL); return NULL; } bool hasMethod(PKVM* vm, Var self, String* name, Closure** _method) { Class* cls = getClass(vm, self); ASSERT(cls != NULL, OOPS); Closure* method_ = clsGetMethod(cls, name); if (method_ != NULL) { *_method = method_; return true; } return false; } Var getMethod(PKVM* vm, Var self, String* name, bool* is_method) { Closure* method; if (hasMethod(vm, self, name, &method)) { if (is_method) *is_method = true; return VAR_OBJ(method); } // If the attribute not found it'll set an error. if (is_method) *is_method = false; return varGetAttrib(vm, self, name); } Closure* getSuperMethod(PKVM* vm, Var self, String* name) { Class* super = getClass(vm, self)->super_class; if (super == NULL) { VM_SET_ERROR(vm, stringFormat(vm, "'$' object has no parent class.", \ varTypeName(self))); return NULL; }; Closure* method = clsGetMethod(super, name); if (method == NULL) { VM_SET_ERROR(vm, stringFormat(vm, "'@' class has no method named '@'.", \ super->name, name)); } return method; } #define UNSUPPORTED_UNARY_OP(op) \ VM_SET_ERROR(vm, stringFormat(vm, "Unsupported operand ($) for " \ "unary operator " op ".", varTypeName(v))) #define UNSUPPORTED_BINARY_OP(op) \ VM_SET_ERROR(vm, stringFormat(vm, "Unsupported operand types for " \ "operator '" op "' $ and $", varTypeName(v1), varTypeName(v2))) #define RIGHT_OPERAND "Right operand" #define CHECK_NUMERIC_OP(op) \ do { \ double n1, n2; \ if (isNumeric(v1, &n1)) { \ if (validateNumeric(vm, v2, &n2, RIGHT_OPERAND)) { \ return VAR_NUM(n1 op n2); \ } \ return VAR_NULL; \ } \ } while (false) #define CHECK_BITWISE_OP(op) \ do { \ int64_t i1, i2; \ if (isInteger(v1, &i1)) { \ if (validateInteger(vm, v2, &i2, RIGHT_OPERAND)) { \ return VAR_NUM((double)(i1 op i2)); \ } \ return VAR_NULL; \ } \ } while (false) #define CHECK_INST_UNARY_OP(name) \ do { \ if (IS_OBJ_TYPE(v, OBJ_INST)) { \ Var result; \ if (_callUnaryOpMethod(vm, v, name, &result)) { \ return result; \ } \ } \ } while (false) #define CHECK_INST_BINARY_OP(name) \ do { \ if (IS_OBJ_TYPE(v1, OBJ_INST)) { \ Var result; \ if (inplace) { \ if (_callBinaryOpMethod(vm, v1, v2, name "=", &result)) { \ return result; \ } \ } \ if (_callBinaryOpMethod(vm, v1, v2, name, &result)) { \ return result; \ } \ } \ } while(false) Var varPositive(PKVM* vm, Var v) { double n; if (isNumeric(v, &n)) return v; CHECK_INST_UNARY_OP("+self"); UNSUPPORTED_UNARY_OP("unary +"); return VAR_NULL; } Var varNegative(PKVM* vm, Var v) { double n; if (isNumeric(v, &n)) return VAR_NUM(-AS_NUM(v)); CHECK_INST_UNARY_OP("-self"); UNSUPPORTED_UNARY_OP("unary -"); return VAR_NULL; } Var varNot(PKVM* vm, Var v) { CHECK_INST_UNARY_OP("!self"); return VAR_BOOL(!toBool(v)); } Var varBitNot(PKVM* vm, Var v) { int64_t i; if (isInteger(v, &i)) return VAR_NUM((double)(~i)); CHECK_INST_UNARY_OP("~self"); UNSUPPORTED_UNARY_OP("unary ~"); return VAR_NULL; } Var varAdd(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_NUMERIC_OP(+); if (IS_OBJ(v1)) { Object *o1 = AS_OBJ(v1); switch (o1->type) { case OBJ_STRING: { if (!IS_OBJ(v2)) break; Object* o2 = AS_OBJ(v2); if (o2->type == OBJ_STRING) { return VAR_OBJ(stringJoin(vm, (String*)o1, (String*)o2)); } } break; case OBJ_LIST: { if (!IS_OBJ(v2)) break; Object* o2 = AS_OBJ(v2); if (o2->type == OBJ_LIST) { if (inplace) { pkVarBufferConcat(&((List*)o1)->elements, vm, &((List*)o2)->elements); return v1; } else { return VAR_OBJ(listAdd(vm, (List*)o1, (List*)o2)); } } } break; } } CHECK_INST_BINARY_OP("+"); UNSUPPORTED_BINARY_OP("+"); return VAR_NULL; } Var varModulo(PKVM* vm, Var v1, Var v2, bool inplace) { double n1, n2; if (isNumeric(v1, &n1)) { if (validateNumeric(vm, v2, &n2, RIGHT_OPERAND)) { return VAR_NUM(fmod(n1, n2)); } return VAR_NULL; } if (IS_OBJ_TYPE(v1, OBJ_STRING)) { TODO; // "fmt" % v2. } CHECK_INST_BINARY_OP("%"); UNSUPPORTED_BINARY_OP("%"); return VAR_NULL; } // TODO: the bellow function definitions can be written as macros. Var varSubtract(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_NUMERIC_OP(-); CHECK_INST_BINARY_OP("-"); UNSUPPORTED_BINARY_OP("-"); return VAR_NULL; } Var varMultiply(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_NUMERIC_OP(*); CHECK_INST_BINARY_OP("*"); UNSUPPORTED_BINARY_OP("*"); return VAR_NULL; } Var varDivide(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_NUMERIC_OP(/); CHECK_INST_BINARY_OP("/"); UNSUPPORTED_BINARY_OP("/"); return VAR_NULL; } Var varBitAnd(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_BITWISE_OP(&); CHECK_INST_BINARY_OP("&"); UNSUPPORTED_BINARY_OP("&"); return VAR_NULL; } Var varBitOr(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_BITWISE_OP(|); CHECK_INST_BINARY_OP("|"); UNSUPPORTED_BINARY_OP("|"); return VAR_NULL; } Var varBitXor(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_BITWISE_OP(^); CHECK_INST_BINARY_OP("^"); UNSUPPORTED_BINARY_OP("^"); return VAR_NULL; } Var varBitLshift(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_BITWISE_OP(<<); CHECK_INST_BINARY_OP("<<"); UNSUPPORTED_BINARY_OP("<<"); return VAR_NULL; } Var varBitRshift(PKVM* vm, Var v1, Var v2, bool inplace) { CHECK_BITWISE_OP(>>); CHECK_INST_BINARY_OP(">>"); UNSUPPORTED_BINARY_OP(">>"); return VAR_NULL; } Var varEqals(PKVM* vm, Var v1, Var v2) { const bool inplace = false; CHECK_INST_BINARY_OP("=="); return VAR_BOOL(isValuesEqual(v1, v2)); } Var varGreater(PKVM* vm, Var v1, Var v2) { CHECK_NUMERIC_OP(>); const bool inplace = false; CHECK_INST_BINARY_OP(">"); UNSUPPORTED_BINARY_OP(">"); return VAR_NULL; } Var varLesser(PKVM* vm, Var v1, Var v2) { CHECK_NUMERIC_OP(<); const bool inplace = false; CHECK_INST_BINARY_OP("<"); UNSUPPORTED_BINARY_OP("<"); return VAR_NULL; } Var varOpRange(PKVM* vm, Var v1, Var v2) { if (IS_NUM(v1) && IS_NUM(v2)) { return VAR_OBJ(newRange(vm, AS_NUM(v1), AS_NUM(v2))); } const bool inplace = false; CHECK_INST_BINARY_OP(".."); UNSUPPORTED_BINARY_OP(".."); return VAR_NULL; } #undef RIGHT_OPERAND #undef CHECK_NUMERIC_OP #undef CHECK_BITWISE_OP #undef UNSUPPORTED_UNARY_OP #undef UNSUPPORTED_BINARY_OP bool varContains(PKVM* vm, Var elem, Var container) { if (!IS_OBJ(container)) { VM_SET_ERROR(vm, stringFormat(vm, "'$' is not iterable.", varTypeName(container))); } Object* obj = AS_OBJ(container); switch (obj->type) { case OBJ_STRING: { if (!IS_OBJ_TYPE(elem, OBJ_STRING)) { VM_SET_ERROR(vm, stringFormat(vm, "Expected a string operand.")); return false; } String* sub = (String*)AS_OBJ(elem); String* str = (String*)AS_OBJ(container); if (sub->length > str->length) return false; TODO; } break; case OBJ_LIST: { List* list = (List*)AS_OBJ(container); for (uint32_t i = 0; i < list->elements.count; i++) { if (isValuesEqual(elem, list->elements.data[i])) return true; } return false; } break; case OBJ_MAP: { Map* map = (Map*)AS_OBJ(container); return !IS_UNDEF(mapGet(map, elem)); } break; } #define v1 container #define v2 elem const bool inplace = false; CHECK_INST_BINARY_OP("in"); #undef v1 #undef v2 VM_SET_ERROR(vm, stringFormat(vm, "Argument of type $ is not iterable.", varTypeName(container))); return VAR_NULL; } bool varIsType(PKVM* vm, Var inst, Var type) { if (!IS_OBJ_TYPE(type, OBJ_CLASS)) { VM_SET_ERROR(vm, newString(vm, "Right operand must be a class.")); return VAR_NULL; } Class* cls = (Class*)AS_OBJ(type); Class* cls_inst = getClass(vm, inst); do { if (cls_inst == cls) return true; cls_inst = cls_inst->super_class; } while (cls_inst != NULL); return false; } Var varGetAttrib(PKVM* vm, Var on, String* attrib) { #define ERR_NO_ATTRIB(vm, on, attrib) \ VM_SET_ERROR(vm, stringFormat(vm, "'$' object has no attribute named '$'.", \ varTypeName(on), attrib->data)) if (!IS_OBJ(on)) { VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return VAR_NULL; } Object* obj = AS_OBJ(on); switch (obj->type) { case OBJ_STRING: { String* str = (String*)obj; switch (attrib->hash) { case CHECK_HASH("length", 0x83d03615): return VAR_NUM((double)(str->length)); case CHECK_HASH("lower", 0xb51d04ba): return VAR_OBJ(stringLower(vm, str)); case CHECK_HASH("upper", 0xa8c6a47): return VAR_OBJ(stringUpper(vm, str)); case CHECK_HASH("strip", 0xfd1b18d1): return VAR_OBJ(stringStrip(vm, str)); } } break; case OBJ_LIST: { List* list = (List*)obj; switch (attrib->hash) { case CHECK_HASH("length", 0x83d03615): return VAR_NUM((double)(list->elements.count)); } } break; case OBJ_MAP: { // map = { "foo" : 42, "can't access" : 32 } // val = map.foo ## <-- This should be error // Only the map's attributes are accessed here. TODO; } break; case OBJ_RANGE: { Range* range = (Range*)obj; switch (attrib->hash) { case CHECK_HASH("as_list", 0x1562c22): return VAR_OBJ(rangeAsList(vm, range)); // We can't use 'start', 'end' since 'end' in pocketlang is a // keyword. Also we can't use 'from', 'to' since 'from' is a keyword // too. So, we're using 'first' and 'last' to access the range limits. case CHECK_HASH("first", 0x4881d841): return VAR_NUM(range->from); case CHECK_HASH("last", 0x63e1d819): return VAR_NUM(range->to); } } break; case OBJ_MODULE: { Module* module = (Module*)obj; // Search in globals. int index = moduleGetGlobalIndex(module, attrib->data, attrib->length); if (index != -1) { ASSERT_INDEX((uint32_t)index, module->globals.count); return module->globals.data[index]; } } break; case OBJ_FUNC: break; case OBJ_CLOSURE: { Closure* closure = (Closure*)obj; switch (attrib->hash) { case CHECK_HASH("arity", 0x3e96bd7a): return VAR_NUM((double)(closure->fn->arity)); case CHECK_HASH("name", 0x8d39bde6): return VAR_OBJ(newString(vm, closure->fn->name)); } } break; case OBJ_UPVALUE: UNREACHABLE(); // Upvalues aren't first class objects. break; case OBJ_FIBER: { Fiber* fb = (Fiber*)obj; switch (attrib->hash) { case CHECK_HASH("is_done", 0x789c2706): return VAR_BOOL(fb->state == FIBER_DONE); case CHECK_HASH("function", 0x9ed64249): return VAR_OBJ(fb->closure); } } break; case OBJ_CLASS: TODO; break; case OBJ_INST: { Instance* inst = (Instance*)obj; Var value = VAR_NULL; if (inst->native != NULL) { Closure* getter; // TODO: static vm string? String* getter_name = newString(vm, GETTER_NAME); vmPushTempRef(vm, &getter_name->_super); // getter_name. bool has_getter = hasMethod(vm, on, getter_name, &getter); vmPopTempRef(vm); // getter_name. if (has_getter) { Var attrib_name = VAR_OBJ(attrib); vmCallMethod(vm, on, getter, 1, &attrib_name, &value); return value; // If any error occure, it was already set. } } value = mapGet(inst->attribs, VAR_OBJ(attrib)); if (!IS_UNDEF(value)) return value; } break; } ERR_NO_ATTRIB(vm, on, attrib); return VAR_NULL; #undef ERR_NO_ATTRIB } void varSetAttrib(PKVM* vm, Var on, String* attrib, Var value) { // Set error for accessing non-existed attribute. #define ERR_NO_ATTRIB(vm, on, attrib) \ VM_SET_ERROR(vm, stringFormat(vm, \ "'$' object has no mutable attribute named '$'", \ varTypeName(on), attrib->data)) #define ATTRIB_IMMUTABLE(name) \ do { \ if ((attrib->length == strlen(name) && strcmp(name, attrib->data) == 0)) { \ VM_SET_ERROR(vm, stringFormat(vm, "'$' attribute is immutable.", name)); \ return; \ } \ } while (false) if (!IS_OBJ(on)) { VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return; } Object* obj = AS_OBJ(on); switch (obj->type) { case OBJ_STRING: ATTRIB_IMMUTABLE("length"); ATTRIB_IMMUTABLE("lower"); ATTRIB_IMMUTABLE("upper"); ATTRIB_IMMUTABLE("strip"); ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_LIST: ATTRIB_IMMUTABLE("length"); ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_MAP: TODO; ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_RANGE: ATTRIB_IMMUTABLE("as_list"); ATTRIB_IMMUTABLE("first"); ATTRIB_IMMUTABLE("last"); ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_MODULE: { Module* module = (Module*)obj; int index = moduleGetGlobalIndex(module, attrib->data, attrib->length); if (index != -1) { ASSERT_INDEX((uint32_t)index, module->globals.count); module->globals.data[index] = value; return; } } break; case OBJ_FUNC: UNREACHABLE(); // Functions aren't first class objects. return; case OBJ_CLOSURE: ATTRIB_IMMUTABLE("arity"); ATTRIB_IMMUTABLE("name"); ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_UPVALUE: UNREACHABLE(); // Upvalues aren't first class objects. return; case OBJ_FIBER: ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_CLASS: ERR_NO_ATTRIB(vm, on, attrib); return; case OBJ_INST: { Instance* inst = (Instance*)obj; if (inst->native != NULL) { Closure* setter; // TODO: static vm string? String* setter_name = newString(vm, SETTER_NAME); vmPushTempRef(vm, &setter_name->_super); // setter_name. bool has_setter = hasMethod(vm, VAR_OBJ(inst), setter_name, &setter); vmPopTempRef(vm); // setter_name. if (has_setter) { // FIXME: // Once we retreive values from directly the stack we can pass the // args pointer, pointing in the VM stack, instead of creating a temp // array as bellow. Var args[2] = { VAR_OBJ(attrib), value }; vmCallMethod(vm, VAR_OBJ(inst), setter, 2, args, NULL); return; // If any error occure, it was already set. } } mapSet(vm, inst->attribs, VAR_OBJ(attrib), value); return; } break; } ERR_NO_ATTRIB(vm, on, attrib); return; #undef ATTRIB_IMMUTABLE #undef ERR_NO_ATTRIB } Var varGetSubscript(PKVM* vm, Var on, Var key) { if (!IS_OBJ(on)) { VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return VAR_NULL; } Object* obj = AS_OBJ(on); switch (obj->type) { case OBJ_STRING: { int64_t index; String* str = ((String*)obj); if (!validateInteger(vm, key, &index, "List index")) { return VAR_NULL; } if (!validateIndex(vm, index, str->length, "String")) { return VAR_NULL; } String* c = newStringLength(vm, str->data + index, 1); return VAR_OBJ(c); } break; case OBJ_LIST: { int64_t index; pkVarBuffer* elems = &((List*)obj)->elements; if (!validateInteger(vm, key, &index, "List index")) { return VAR_NULL; } if (!validateIndex(vm, index, elems->count, "List")) { return VAR_NULL; } return elems->data[index]; } break; case OBJ_MAP: { Var value = mapGet((Map*)obj, key); if (IS_UNDEF(value)) { if (IS_OBJ(key) && !isObjectHashable(AS_OBJ(key)->type)) { VM_SET_ERROR(vm, stringFormat(vm, "Unhashable key '$'.", varTypeName(key))); } else { String* key_repr = toRepr(vm, key); vmPushTempRef(vm, &key_repr->_super); // key_repr. VM_SET_ERROR(vm, stringFormat(vm, "Key '@' not exists", key_repr)); vmPopTempRef(vm); // key_repr. } return VAR_NULL; } return value; } break; case OBJ_FUNC: case OBJ_UPVALUE: UNREACHABLE(); // Not first class objects. } VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return VAR_NULL; } void varsetSubscript(PKVM* vm, Var on, Var key, Var value) { if (!IS_OBJ(on)) { VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return; } Object* obj = AS_OBJ(on); switch (obj->type) { case OBJ_LIST: { int64_t index; pkVarBuffer* elems = &((List*)obj)->elements; if (!validateInteger(vm, key, &index, "List index")) return; if (!validateIndex(vm, index, elems->count, "List")) return; elems->data[index] = value; return; } break; case OBJ_MAP: { if (IS_OBJ(key) && !isObjectHashable(AS_OBJ(key)->type)) { VM_SET_ERROR(vm, stringFormat(vm, "$ type is not hashable.", varTypeName(key))); } else { mapSet(vm, (Map*)obj, key, value); } return; } break; case OBJ_FUNC: case OBJ_UPVALUE: UNREACHABLE(); } VM_SET_ERROR(vm, stringFormat(vm, "$ type is not subscriptable.", varTypeName(on))); return; }