diff --git a/cli/common.h b/cli/common.h index 7c8557e..27bc420 100644 --- a/cli/common.h +++ b/cli/common.h @@ -20,7 +20,7 @@ "Free and open source software under the terms of the MIT license.\n" // Note that the cli itself is not a part of the pocketlang compiler, instead -// its a host application to run pocketlang from the command line. We're +// it's a host application to run pocketlang from the command line. We're // embedding the pocketlang VM and we can only use its public APIs, not any // internals of it, including assertion macros. So we're re-defining those // macros here (like if it's a new project). @@ -34,8 +34,7 @@ // The internal assertion macro, this will print error and break regardless of // the build target (debug or release). Use ASSERT() for debug assertion and -// use __ASSERT() for TODOs and assetions in public methods (to indicate that -// the host application did something wrong). +// use __ASSERT() for TODOs. #define __ASSERT(condition, message) \ do { \ if (!(condition)) { \ @@ -65,6 +64,7 @@ do { \ fprintf(stderr, "Execution reached an unreachable path\n" \ "\tat %s() (%s:%i)\n", __func__, __FILE__, __LINE__); \ + DEBUG_BREAK(); \ abort(); \ } while (false) diff --git a/cli/modules/path.c b/cli/modules/path.c index 0927a8d..0caa80c 100644 --- a/cli/modules/path.c +++ b/cli/modules/path.c @@ -3,7 +3,6 @@ * Distributed Under The MIT License */ - #include #include #include /* defines FILENAME_MAX */ @@ -29,7 +28,6 @@ // TODO: No error is handled below. I should check for path with size more than // FILENAME_MAX. - // TODO: this macros should be moved to a general place of in cli. #define TOSTRING(x) #x @@ -64,7 +62,6 @@ size_t pathJoin(const char* path_a, const char* path_b, char* buffer, return cwk_path_join(path_a, path_b, buffer, buff_size); } - /*****************************************************************************/ /* INTERNAL FUNCTIONS */ /*****************************************************************************/ diff --git a/cli/repl.c b/cli/repl.c index 2215d57..aab99b0 100644 --- a/cli/repl.c +++ b/cli/repl.c @@ -3,16 +3,14 @@ * Distributed Under The MIT License */ - // The REPL (Read Evaluate Print Loop) implementation. -// https://en.wikipedia.org/wiki/Read–eval–print_loop. +// https://en.wikipedia.org/wiki/Read-eval-print_loop. #include "common.h" #include // isspace #include "utils.h" - // FIXME: use fgetc char by char till reach a new line. // // Read a line from stdin and returns it without the line ending. Accepting @@ -55,8 +53,6 @@ int repl(PKVM* vm, const PkCompileOptions* options) { // The main module that'll be used to compile and execute the input source. PkHandle* module = pkNewModule(vm, "$(REPL)"); - // FIXME: Again it's temp for testing. - // A buffer to store lines read from stdin. ByteBuffer lines; byteBufferInit(&lines); @@ -120,6 +116,7 @@ int repl(PKVM* vm, const PkCompileOptions* options) { } while (!done); + byteBufferClear(&lines); pkReleaseHandle(vm, module); return 0; diff --git a/cli/utils.c b/cli/utils.c index 2a62bb7..55995e8 100644 --- a/cli/utils.c +++ b/cli/utils.c @@ -26,7 +26,6 @@ static inline int powerOf2Ceil(int n) { /* BYTE BUFFER IMPLEMENTATION */ /*****************************************************************************/ - void byteBufferInit(ByteBuffer* buffer) { buffer->data = NULL; buffer->count = 0; @@ -34,7 +33,7 @@ void byteBufferInit(ByteBuffer* buffer) { } void byteBufferClear(ByteBuffer* buffer) { - buffer->data = realloc(buffer->data, 0); + free(buffer->data); buffer->data = NULL; buffer->count = 0; buffer->capacity = 0; diff --git a/cli/utils.h b/cli/utils.h index 1fb0bb2..2d64b49 100644 --- a/cli/utils.h +++ b/cli/utils.h @@ -11,12 +11,11 @@ typedef struct { uint32_t capacity; } ByteBuffer; - /*****************************************************************************/ /* BYTE BUFFER */ /*****************************************************************************/ -// Initialize a new buffer int instance. +// Initialize a new buffer int instance. void byteBufferInit(ByteBuffer* buffer); // Clears the allocated elements from the VM's realloc function. diff --git a/docs/TODO.txt b/docs/TODO.txt index 50e93a1..985a17b 100644 --- a/docs/TODO.txt +++ b/docs/TODO.txt @@ -7,9 +7,13 @@ - check for tabs and trailing white space in source. they're not allowed. +- Hex, binary literals and floats like ".5". + - change or add => to_string() to value.as_string and add as_repr, as_bool. - str_lower("UPPER") to "UPPER".lower + +- Make bool and num are incompatible + 1 + true - make it not allowed. - support new line just after '=' (and other places) @@ -25,7 +29,6 @@ - Implement utf8 support. - Implement gdb like debugger (add color print for readability). - Initialize imported scripts (require fiber based vm). -- Hex, binary literals and floats like ".5". - Complete all the TODO; macros. - implement MAX_ARGC checks (would cause a buffer overflow if not) when compiling and calling a function (also in fibers). diff --git a/src/include/pocketlang.h b/src/include/pocketlang.h index e3517eb..faf0be5 100644 --- a/src/include/pocketlang.h +++ b/src/include/pocketlang.h @@ -57,13 +57,14 @@ extern "C" { // A convinent macro to define documentation of funcions. Use it to document // your native functions. // -// PK_DOC(foo, -// "The function will print 'foo' on the console.") { +// PK_DOC( +// "The function will print 'foo' on the console.", +// static void foo()) { // printf("foo\n"); // } // -#define PK_DOC(func, doc) \ - /* TODO: static char __pkdoc__##func[] = doc;*/ static void func(PKVM* vm) +#define PK_DOC(doc, func) \ + /* TODO: static char __pkdoc__##func[] = doc;*/ func // Name of the implicit function for a module. When a module is parsed all of // it's statements are wrapped around an implicit function with this name. diff --git a/src/pk_buffers.h b/src/pk_buffers.h index b471239..86ec567 100644 --- a/src/pk_buffers.h +++ b/src/pk_buffers.h @@ -43,7 +43,6 @@ void pk##m_name##BufferWrite(pk##m_name##Buffer* self, \ PKVM* vm, m_type data); \ - // The buffer "template" implementation of diferent types. #define DEFINE_BUFFER(m_name, m_type) \ void pk##m_name##BufferInit(pk##m_name##Buffer* self) { \ diff --git a/src/pk_common.h b/src/pk_common.h index cf3eb71..614805c 100644 --- a/src/pk_common.h +++ b/src/pk_common.h @@ -113,6 +113,7 @@ do { \ fprintf(stderr, "Execution reached an unreachable path\n" \ "\tat %s() (%s:%i)\n", __func__, __FILE__, __LINE__); \ + DEBUG_BREAK(); \ abort(); \ } while (false) @@ -141,7 +142,7 @@ // Using __ASSERT() for make it crash in release binary too. #define TODO __ASSERT(false, "TODO: It hasn't implemented yet.") -#define OOPS "Oops a bug!! report plese." +#define OOPS "Oops a bug!! report please." #define TOSTRING(x) #x #define STRINGIFY(x) TOSTRING(x) @@ -149,6 +150,7 @@ // The formated string to convert double to string. It'll be with the minimum // length string representation of either a regular float or a scientific // notation (at most 15 decimal points). +// Reference: https://www.cplusplus.com/reference/cstdio/printf/ #define DOUBLE_FMT "%.16g" // Double number to string buffer size, used in sprintf with DOUBLE_FMT. @@ -169,4 +171,23 @@ // + 1 for null byte '\0' #define STR_INT_BUFF_SIZE 12 +// Integer number (double) to hex string buffer size. +// The maximum value an unsigned 64 bit integer can get is +// 0xffffffffffffffff which is 16 characters. +// + 16 for hex digits +// + 1 for sign '-' +// + 2 for '0x' prefix +// + 1 for null byte '\0' +#define STR_HEX_BUFF_SIZE 20 + +// Integer number (double) to bin string buffer size. +// The maximum value an unsigned 64 bit integer can get is +// 0b1111111111111111111111111111111111111111111111111111111111111111 +// which is 64 characters. +// + 64 for bin digits +// + 1 for sign '-' +// + 2 for '0b' prefix +// + 1 for null byte '\0' +#define STR_BIN_BUFF_SIZE 68 + #endif //PK_COMMON_H diff --git a/src/pk_compiler.c b/src/pk_compiler.c index 3f249ad..3d5eca4 100644 --- a/src/pk_compiler.c +++ b/src/pk_compiler.c @@ -194,7 +194,6 @@ static _Keyword _keywords[] = { { NULL, 0, (TokenType)(0) }, // Sentinal to mark the end of the array }; - /***************************************************************************** * COMPILIER INTERNAL TYPES * *****************************************************************************/ @@ -302,7 +301,7 @@ typedef struct sForwardName { typedef struct sFunc { - // Scope of the function. -2 for script body, -1 for top level function and + // Scope of the function. -2 for script body, -1 for top level function and // literal functions will have the scope where it declared. int depth; @@ -354,7 +353,7 @@ struct Compiler { int forwards_count; // True if the last statement is a new local variable assignment. Because - // the assignment is different than reqular assignment and use this boolean + // the assignment is different than reqular assignment and use this boolean // to tell the compiler that dont pop it's assigned value because the value // itself is the local. bool new_local; @@ -409,8 +408,8 @@ static void reportError(Compiler* compiler, const char* file, int line, char message[ERROR_MESSAGE_SIZE]; int length = vsprintf(message, fmt, args); - __ASSERT(length < ERROR_MESSAGE_SIZE, "Error message buffer should not exceed " - "the buffer"); + __ASSERT(length < ERROR_MESSAGE_SIZE, "Error message buffer should not " + "exceed the buffer"); vm->config.error_fn(vm, PK_ERROR_COMPILE, file, line, message); } @@ -917,7 +916,7 @@ static NameSearchResult compilerSearchName(Compiler* compiler, result.index = index; return result; } - + // Search through functions. index = scriptGetFunc(compiler->script, name, length); if (index != -1) { @@ -1194,16 +1193,16 @@ static void exprName(Compiler* compiler) { } /* a or b: | a and b: - | + | (...) | (...) .-- jump_if [offset] | .-- jump_if_not [offset] - | (...) | | (...) + | (...) | | (...) |-- jump_if [offset] | |-- jump_if_not [offset] - | push false | | push true + | push false | | push true .--+-- jump [offset] | .--+-- jump [offset] | '-> push true | | '-> push false '----> (...) | '----> (...) -*/ +*/ void exprOr(Compiler* compiler) { emitOpcode(compiler, OP_JUMP_IF); @@ -1214,7 +1213,7 @@ void exprOr(Compiler* compiler) { int true_offset_b = emitShort(compiler, 0xffff); //< Will be patched. emitOpcode(compiler, OP_PUSH_FALSE); - emitOpcode(compiler, OP_JUMP); + emitOpcode(compiler, OP_JUMP); int end_offset = emitShort(compiler, 0xffff); //< Will be patched. patchJump(compiler, true_offset_a); @@ -1486,7 +1485,7 @@ static void parsePrecedence(Compiler* compiler, Precedence precedence) { static void compilerInit(Compiler* compiler, PKVM* vm, const char* source, Script* script, const PkCompileOptions* options) { - + compiler->vm = vm; compiler->next_compiler = NULL; @@ -1559,10 +1558,10 @@ static int compilerAddVariable(Compiler* compiler, const char* name, // Add the variable and return it's index. if (compiler->scope_depth == DEPTH_GLOBAL) { - uint32_t name_index = scriptAddName(compiler->script, compiler->vm, - name, length); - pkUintBufferWrite(&compiler->script->global_names, compiler->vm, name_index); - pkVarBufferWrite(&compiler->script->globals, compiler->vm, VAR_NULL); + Script* script = compiler->script; + uint32_t name_index = scriptAddName(script, compiler->vm, name, length); + pkUintBufferWrite(&script->global_names, compiler->vm, name_index); + pkVarBufferWrite(&script->globals, compiler->vm, VAR_NULL); return compiler->script->globals.count - 1; } else { @@ -1726,7 +1725,7 @@ static int compileFunction(Compiler* compiler, FuncType fn_type) { name = LITERAL_FN_NAME; name_length = (int)strlen(name); } - + Function* func = newFunction(compiler->vm, name, name_length, compiler->script, fn_type == FN_NATIVE); int fn_index = (int)compiler->script->functions.count - 1; @@ -1784,7 +1783,7 @@ static int compileFunction(Compiler* compiler, FuncType fn_type) { if (fn_type != FN_NATIVE) { compileBlockBody(compiler, BLOCK_FUNC); consume(compiler, TK_END, "Expected 'end' after function definition end."); - + // TODO: This is the function end return, if we pop all the parameters the // below push_null is redundent (because we always have a null at the rbp // of the call frame. (for i in argc : emit(pop)) emit(return); but this @@ -2058,22 +2057,22 @@ static void compileFromImport(Compiler* compiler) { const char* name = compiler->previous.start; int length = compiler->previous.length; int line = compiler->previous.line; - + // Add the name of the symbol to the names buffer. int name_index = (int)scriptAddName(compiler->script, compiler->vm, name, length); - + // Don't pop the lib since it'll be used for the next entry. emitOpcode(compiler, OP_GET_ATTRIB_KEEP); emitShort(compiler, name_index); //< Name of the attrib. - + // Check if it has an alias. if (match(compiler, TK_AS)) { // Consuming it'll update the previous token which would be the name of // the binding variable. consume(compiler, TK_NAME, "Expected a name after 'as'."); } - + // Get the variable to bind the imported symbol, if we already have a // variable with that name override it, otherwise use a new variable. const char* name_start = compiler->previous.start; @@ -2085,7 +2084,7 @@ static void compileFromImport(Compiler* compiler) { emitStoreVariable(compiler, var_index, true); emitOpcode(compiler, OP_POP); - + } while (match(compiler, TK_COMMA) && (skipNewLines(compiler), true)); } @@ -2465,7 +2464,7 @@ PkResult compile(PKVM* vm, Script* script, const char* source, skipNewLines(compiler); } - // Already a null at the stack top, added when the fiber for the function created. + emitOpcode(compiler, OP_PUSH_NULL); emitOpcode(compiler, OP_RETURN); emitOpcode(compiler, OP_END); @@ -2498,7 +2497,7 @@ PkResult compile(PKVM* vm, Script* script, const char* source, #if DEBUG_DUMP_COMPILED_CODE dumpFunctionCode(vm, script->body); #endif - + // Return the compilation result. if (compiler->has_errors) { if (compiler->options && compiler->options->repl_mode && @@ -2523,7 +2522,7 @@ PkResult pkCompileModule(PKVM* vm, PkHandle* module, PkStringPtr source, } void compilerMarkObjects(PKVM* vm, Compiler* compiler) { - + // Mark the script which is currently being compiled. grayObject(vm, &compiler->script->_super); diff --git a/src/pk_compiler.h b/src/pk_compiler.h index cd6e9ed..7b6faa2 100644 --- a/src/pk_compiler.h +++ b/src/pk_compiler.h @@ -19,9 +19,10 @@ typedef enum { // doesn't go through the basic compilation pipeline such as lexing, parsing // (AST), analyzing, intermediate code generation, and target codegeneration // one by one. Instead it'll generate the target code as it reads the source -// (directly from lexing to codegen). Despite it faster than multipass compilers, -// we're restricted syntax-wise and from compile-time optimizations, yet we support -// "forward names" to call functions before they defined (unlike C/Python). +// (directly from lexing to codegen). Despite it's faster than multipass +// compilers, we're restricted syntax-wise and from compile-time optimizations. +// Yet we support "forward names" to call functions before they defined +// (unlike C/Python). typedef struct Compiler Compiler; // This will take source code as a cstring, compiles it to pocketlang bytecodes diff --git a/src/pk_core.c b/src/pk_core.c index 7ad3fcd..cd916ac 100644 --- a/src/pk_core.c +++ b/src/pk_core.c @@ -50,13 +50,13 @@ PkHandle* pkGetFunction(PKVM* vm, PkHandle* module, // TODO: Currently it's O(n) and could be optimized to O(log(n)) but does it // worth it? - // + // // 'function_names' buffer is un-necessary since the function itself has the - // reference to the function name and it can be refactored into a index buffer - // in an "increasing-name" order which can be used to binary search. Similer - // for 'global_names' refactor them from VarBuffer to GlobalVarBuffer where - // GlobalVar is struct { const char* name, Var value }; - // + // reference to the function name and it can be refactored into a index + // buffer in an "increasing-name" order which can be used to binary search. + // Similer for 'global_names' refactor them from VarBuffer to GlobalVarBuffer + // where GlobalVar is struct { const char* name, Var value }; + // // "increasing-name" order index buffer: // A buffer of int where each is an index in the function buffer and each // points to different functions in an "increasing-name" (could be hash @@ -72,14 +72,10 @@ PkHandle* pkGetFunction(PKVM* vm, PkHandle* module, return NULL; } -// A convenient macro to get the nth (1 based) argument of the current function. +// A convenient macro to get the nth (1 based) argument of the current +// function. #define ARG(n) (vm->fiber->ret[n]) -// Convenient macros to get the 1st, 2nd, 3rd arguments. -#define ARG1 ARG(1) -#define ARG2 ARG(2) -#define ARG3 ARG(3) - // Evaluates to the current function's argument count. #define ARGC ((int)(vm->fiber->sp - vm->fiber->ret) - 1) @@ -102,7 +98,7 @@ PkHandle* pkGetFunction(PKVM* vm, PkHandle* module, __ASSERT(vm->fiber != NULL, \ "This function can only be called at runtime."); \ __ASSERT(arg > 0 || arg <= ARGC, "Invalid argument index."); \ - __ASSERT(value != NULL, "Parameter [value] was NULL."); \ + __ASSERT(value != NULL, "Argument [value] was NULL."); \ } while (false) // Set error for incompatible type provided as an argument. @@ -265,26 +261,29 @@ static inline bool validateNumeric(PKVM* vm, Var var, double* value, return false; } -// Check if [var] is integer. If not set error and return false. -static inline bool validateInteger(PKVM* vm, Var var, int32_t* value, +// Check if [var] is 32 bit integer. If not set error and return false. +static inline bool validateInteger(PKVM* vm, Var var, int64_t* value, const char* name) { double number; if (isNumeric(var, &number)) { - double truncated = floor(number); - if (truncated == number) { - *value = (int32_t)(truncated); + // TODO: check if the number is larger for a 64 bit integer. + double floor_val = floor(number); + if (floor_val == number) { + *value = (int64_t)(floor_val); return true; } } - vm->fiber->error = stringFormat(vm, "$ must be an integer.", name); + vm->fiber->error = stringFormat(vm, "$ must be a whole number.", name); return false; } -static inline bool validateIndex(PKVM* vm, int32_t index, int32_t size, - const char* container) { +// 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->fiber->error = stringFormat(vm, "$ index out of range.", container); + vm->fiber->error = stringFormat(vm, "$ index out of bound.", container); return false; } return true; @@ -351,12 +350,12 @@ Script* getCoreLib(const PKVM* vm, String* name) { #define FN_IS_PRIMITE_TYPE(name, check) \ static void coreIs##name(PKVM* vm) { \ - RET(VAR_BOOL(check(ARG1))); \ + RET(VAR_BOOL(check(ARG(1)))); \ } #define FN_IS_OBJ_TYPE(name, _enum) \ static void coreIs##name(PKVM* vm) { \ - Var arg1 = ARG1; \ + Var arg1 = ARG(1); \ if (IS_OBJ_TYPE(arg1, _enum)) { \ RET(VAR_TRUE); \ } else { \ @@ -376,29 +375,73 @@ FN_IS_OBJ_TYPE(Function, OBJ_FUNC) FN_IS_OBJ_TYPE(Script, OBJ_SCRIPT) FN_IS_OBJ_TYPE(UserObj, OBJ_USER) -PK_DOC(coreTypeName, +PK_DOC( "type_name(value:var) -> string\n" - "Returns the type name of the of the value.") { - RET(VAR_OBJ(newString(vm, varTypeName(ARG1)))); + "Returns the type name of the of the value.", +static void coreTypeName(PKVM* vm)) { + RET(VAR_OBJ(newString(vm, varTypeName(ARG(1))))); } -PK_DOC(coreAssert, +// TODO: Complete this and register it. +PK_DOC( + "bin(value:num) -> string\n" + "Returns as a binary value string with '0x' prefix.", +static void coreBin(PKVM* vm)) { + int64_t value; + if (!validateInteger(vm, ARG(1), &value, "Argument 1")) return; + + char buff[STR_BIN_BUFF_SIZE]; + + char* ptr = buff; + if (value < 0) *ptr++ = '-'; + *ptr++ = '0'; *ptr++ = 'b'; + + TODO; // sprintf(ptr, "%b"); +} + +PK_DOC( + "hex(value:num) -> string\n" + "Returns as a hexadecimal value string with '0x' prefix.", +static void coreHex(PKVM* vm)) { + 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->fiber->error = newString(vm, "Integer is too large."); + RET(VAR_NULL); + } + + 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)) ))); +} + +PK_DOC( "assert(condition:bool [, msg:string]) -> void\n" "If the condition is false it'll terminate the current fiber with the " - "optional error message") { + "optional error message", +static void coreAssert(PKVM* vm)) { int argc = ARGC; if (argc != 1 && argc != 2) { RET_ERR(newString(vm, "Invalid argument count.")); } - if (!toBool(ARG1)) { + if (!toBool(ARG(1))) { String* msg = NULL; if (argc == 2) { - if (AS_OBJ(ARG2)->type != OBJ_STRING) { - msg = toString(vm, ARG2); + if (AS_OBJ(ARG(2))->type != OBJ_STRING) { + msg = toString(vm, ARG(2)); } else { - msg = (String*)AS_OBJ(ARG2); + msg = (String*)AS_OBJ(ARG(2)); } vmPushTempRef(vm, &msg->_super); vm->fiber->error = stringFormat(vm, "Assertion failed: '@'.", msg); @@ -409,31 +452,34 @@ PK_DOC(coreAssert, } } -PK_DOC(coreYield, +PK_DOC( "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.") { + "the yield() would be that value otherwise null.", +static void coreYield(PKVM* vm)) { int argc = ARGC; if (argc > 1) { // yield() or yield(val). RET_ERR(newString(vm, "Invalid argument count.")); } - vmYieldFiber(vm, (argc == 1) ? &ARG1 : NULL); + vmYieldFiber(vm, (argc == 1) ? &ARG(1) : NULL); } -PK_DOC(coreToString, +PK_DOC( "to_string(value:var) -> string\n" - "Returns the string representation of the value.") { - RET(VAR_OBJ(toString(vm, ARG1))); + "Returns the string representation of the value.", +static void coreToString(PKVM* vm)) { + RET(VAR_OBJ(toString(vm, ARG(1)))); } -PK_DOC(corePrint, +PK_DOC( "print(...) -> void\n" "Write each argument as comma seperated to the stdout and ends with a " - "newline.") { + "newline.", +static void corePrint(PKVM* vm)) { // If the host appliaction donesn't provide any write function, discard the // output. if (vm->config.write_fn == NULL) return; @@ -446,10 +492,11 @@ PK_DOC(corePrint, vm->config.write_fn(vm, "\n"); } -PK_DOC(coreInput, +PK_DOC( "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.") { + "an optional argument [msg] and prints it before reading.", +static void coreInput(PKVM* vm)) { int argc = ARGC; if (argc != 1 && argc != 2) { RET_ERR(newString(vm, "Invalid argument count.")); @@ -459,7 +506,7 @@ PK_DOC(coreInput, if (vm->config.read_fn == NULL) return; if (argc == 1) { - vm->config.write_fn(vm, toString(vm, ARG1)->data); + vm->config.write_fn(vm, toString(vm, ARG(1))->data); } PkStringPtr result = vm->config.read_fn(vm); @@ -470,66 +517,26 @@ PK_DOC(coreInput, // String functions. // ----------------- -PK_DOC(coreStrLower, - "str_lower(value:string) -> string\n" - "Returns a lower-case version of the given string.") { - String* str; - if (!validateArgString(vm, 1, &str)) return; - String* result = newStringLength(vm, str->data, str->length); - char* data = result->data; - for (; *data; ++data) *data = (char)tolower(*data); - // Since the string is modified re-hash it. - result->hash = utilHashString(result->data); +// TODO: substring. - RET(VAR_OBJ(result)); -} - -PK_DOC(coreStrUpper, - "str_upper(value:string) -> string\n" - "Returns a upper-case version of the given string.") { - String* str; - if (!validateArgString(vm, 1, &str)) return; - - String* result = newStringLength(vm, str->data, str->length); - char* data = result->data; - for (; *data; ++data) *data = (char)toupper(*data); - // Since the string is modified re-hash it. - result->hash = utilHashString(result->data); - - RET(VAR_OBJ(result)); -} - -PK_DOC(coreStrStrip, - "str_strip(value:string) -> string\n" - "Returns a copy of the string as the leading and trailing white spaces are" - "trimed.") { - String* str; - if (!validateArgString(vm, 1, &str)) return; - - const char* start = str->data; - while (*start && isspace(*start)) start++; - if (*start == '\0') RET(VAR_OBJ(newStringLength(vm, NULL, 0))); - - const char* end = str->data + str->length - 1; - while (isspace(*end)) end--; - - RET(VAR_OBJ(newStringLength(vm, start, (uint32_t)(end - start + 1)))); -} - -PK_DOC(coreStrChr, +PK_DOC( "str_chr(value:number) -> string\n" - "Returns the ASCII string value of the integer argument.") { - int32_t num; - if (!validateInteger(vm, ARG1, &num, "Argument 1")) return; + "Returns the ASCII string value of the integer argument.", +static void coreStrChr(PKVM* vm)) { + int64_t num; + if (!validateInteger(vm, ARG(1), &num, "Argument 1")) return; + + // TODO: validate num is a byte. char c = (char)num; RET(VAR_OBJ(newStringLength(vm, &c, 1))); } -PK_DOC(coreStrOrd, +PK_DOC( "str_ord(value:string) -> number\n" - "Returns integer value of the given ASCII character.") { + "Returns integer value of the given ASCII character.", +static void coreStrOrd(PKVM* vm)) { String* c; if (!validateArgString(vm, 1, &c)) return; if (c->length != 1) { @@ -543,9 +550,10 @@ PK_DOC(coreStrOrd, // List functions. // --------------- -PK_DOC(coreListAppend, +PK_DOC( "list_append(self:List, value:var) -> List\n" - "Append the [value] to the list [self] and return the list.") { + "Append the [value] to the list [self] and return the list.", +static void coreListAppend(PKVM* vm)) { List* list; if (!validateArgList(vm, 1, &list)) return; Var elem = ARG(2); @@ -557,10 +565,11 @@ PK_DOC(coreListAppend, // Map functions. // -------------- -PK_DOC(coreMapRemove, +PK_DOC( "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.") { + "exists, otherwise it'll return null.", +static void coreMapRemove(PKVM* vm)) { Map* map; if (!validateArgMap(vm, 1, &map)) return; Var key = ARG(2); @@ -571,35 +580,39 @@ PK_DOC(coreMapRemove, // Fiber functions. // ---------------- -PK_DOC(coreFiberNew, +PK_DOC( "fiber_new(fn:function) -> fiber\n" - "Create and return a new fiber from the given function [fn].") { + "Create and return a new fiber from the given function [fn].", +static void coreFiberNew(PKVM* vm)) { Function* fn; if (!validateArgFunction(vm, 1, &fn)) return; RET(VAR_OBJ(newFiber(vm, fn))); } -PK_DOC(coreFiberGetFunc, +PK_DOC( "fiber_get_func(fb:fiber) -> function\n" "Retruns the fiber's functions. Which is usefull if you wan't to re-run the " - "fiber, you can get the function and crate a new fiber.") { + "fiber, you can get the function and crate a new fiber.", +static void coreFiberGetFunc(PKVM* vm)) { Fiber* fb; if (!validateArgFiber(vm, 1, &fb)) return; RET(VAR_OBJ(fb->func)); } -PK_DOC(coreFiberIsDone, +PK_DOC( "fiber_is_done(fb:fiber) -> bool\n" - "Returns true if the fiber [fb] is done running and can no more resumed.") { + "Returns true if the fiber [fb] is done running and can no more resumed.", +static void coreFiberIsDone(PKVM* vm)) { Fiber* fb; if (!validateArgFiber(vm, 1, &fb)) return; RET(VAR_BOOL(fb->state == FIBER_DONE)); } -PK_DOC(coreFiberRun, +PK_DOC( "fiber_run(fb:fiber, ...) -> 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.") { + "return value or the yielded value if it's yielded.", +static void coreFiberRun(PKVM* vm)) { int argc = ARGC; if (argc == 0) // Missing the fiber argument. @@ -623,10 +636,11 @@ PK_DOC(coreFiberRun, } } -PK_DOC(coreFiberResume, +PK_DOC( "fiber_resume(fb:fiber) -> 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." ) { + "Return it's return value or the yielded value if it's yielded.", +static void coreFiberResume(PKVM* vm)) { int argc = ARGC; if (argc == 0) // Missing the fiber argument. @@ -661,7 +675,7 @@ static Script* newModuleInternal(PKVM* vm, const char* name) { // hosting application. if (!IS_UNDEF(mapGet(vm->core_libs, VAR_OBJ(_name)))) { vmPopTempRef(vm); // _name - __ASSERT(false, stringFormat(vm, + __ASSERT(false, stringFormat(vm, "A module named '$' already exists", name)->data); } @@ -749,60 +763,61 @@ void stdLangWrite(PKVM* vm) { void stdMathFloor(PKVM* vm) { double num; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; RET(VAR_NUM(floor(num))); } void stdMathCeil(PKVM* vm) { double num; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; RET(VAR_NUM(ceil(num))); } void stdMathPow(PKVM* vm) { double num, ex; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; - if (!validateNumeric(vm, ARG2, &ex, "Parameter 2")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; + if (!validateNumeric(vm, ARG(2), &ex, "Argument 2")) return; RET(VAR_NUM(pow(num, ex))); } void stdMathSqrt(PKVM* vm) { double num; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; RET(VAR_NUM(sqrt(num))); } void stdMathAbs(PKVM* vm) { double num; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; if (num < 0) num = -num; RET(VAR_NUM(num)); } void stdMathSign(PKVM* vm) { double num; - if (!validateNumeric(vm, ARG1, &num, "Parameter 1")) return; + if (!validateNumeric(vm, ARG(1), &num, "Argument 1")) return; if (num < 0) num = -1; else if (num > 0) num = +1; else num = 0; RET(VAR_NUM(num)); } -PK_DOC(stdMathHash, +PK_DOC( "hash(value:var) -> num\n" "Return the hash value of the variable, if it's not hashable it'll " - "return null."); + "return null.", +static void stdMathHash(PKVM* vm)); void stdMathHash(PKVM* vm) { - if (IS_OBJ(ARG1)) { - if (!isObjectHashable(AS_OBJ(ARG1)->type)) { + if (IS_OBJ(ARG(1))) { + if (!isObjectHashable(AS_OBJ(ARG(1))->type)) { RET(VAR_NULL); } } - RET(VAR_NUM((double)varHashValue(ARG1))); + RET(VAR_NUM((double)varHashValue(ARG(1)))); } /*****************************************************************************/ @@ -837,7 +852,7 @@ void initializeCore(PKVM* vm) { INITALIZE_BUILTIN_FN("is_null", coreIsNull, 1); INITALIZE_BUILTIN_FN("is_bool", coreIsBool, 1); INITALIZE_BUILTIN_FN("is_num", coreIsNum, 1); - + INITALIZE_BUILTIN_FN("is_string", coreIsString, 1); INITALIZE_BUILTIN_FN("is_list", coreIsList, 1); INITALIZE_BUILTIN_FN("is_map", coreIsMap, 1); @@ -845,7 +860,8 @@ void initializeCore(PKVM* vm) { INITALIZE_BUILTIN_FN("is_function", coreIsFunction, 1); INITALIZE_BUILTIN_FN("is_script", coreIsScript, 1); INITALIZE_BUILTIN_FN("is_userobj", coreIsUserObj, 1); - + + INITALIZE_BUILTIN_FN("hex", coreHex, 1); INITALIZE_BUILTIN_FN("assert", coreAssert, -1); INITALIZE_BUILTIN_FN("yield", coreYield, -1); INITALIZE_BUILTIN_FN("to_string", coreToString, 1); @@ -853,9 +869,6 @@ void initializeCore(PKVM* vm) { INITALIZE_BUILTIN_FN("input", coreInput, -1); // String functions. - INITALIZE_BUILTIN_FN("str_lower", coreStrLower, 1); - INITALIZE_BUILTIN_FN("str_upper", coreStrUpper, 1); - INITALIZE_BUILTIN_FN("str_strip", coreStrStrip, 1); INITALIZE_BUILTIN_FN("str_chr", coreStrChr, 1); INITALIZE_BUILTIN_FN("str_ord", coreStrOrd, 1); @@ -1022,13 +1035,31 @@ bool varLesser(Var v1, Var v2) { return false; } -// A convinent convenient macro used in varGetAttrib and varSetAttrib. -#define IS_ATTRIB(name) \ - (attrib->length == strlen(name) && strcmp(name, attrib->data) == 0) +// Here we're switching the FNV-1a hash value of the name (cstring). Which is +// an efficient way than having multiple if (attrib == "name"). From O(n) * k +// to O(1) where n is the length of the string and k is the number of string +// comparison. +// +// ex: +// SWITCH_ATTRIB(str) { // str = "length" +// CASE_ATTRIB("length", 0x83d03615) : { return string->length; } +// } +// +// In C++11 this can be achieved (in a better way) with user defined literals +// and constexpr. (Reference from my previous compiler written in C++). +// https://github.com/ThakeeNathees/carbon/blob/89b11800132cbfeedcac0c992593afb5f0357236/include/core/internal.h#L174-L180 +// https://github.com/ThakeeNathees/carbon/blob/454d087f85f7fb9408eb0bc10ae702b8de844648/src/var/_string.cpp#L60-L77 +// +// However there is a python script that's matching the CASE_ATTRIB() macro +// calls and validate if the string and the hash values are matching. +// TODO: port it to the CI/CD process at github actions. +// +#define SWITCH_ATTRIB(name) switch (utilHashString(name)) +#define CASE_ATTRIB(name, hash) case hash // Set error for accessing non-existed attribute. -#define ERR_NO_ATTRIB() \ - vm->fiber->error = stringFormat(vm, "'$' objects has no attribute " \ +#define ERR_NO_ATTRIB(on) \ + vm->fiber->error = stringFormat(vm, "'$' object has no attribute " \ "named '$'", \ varTypeName(on), attrib->data); @@ -1044,57 +1075,68 @@ Var varGetAttrib(PKVM* vm, Var on, String* attrib) { switch (obj->type) { case OBJ_STRING: { - if (IS_ATTRIB("length")) { - size_t length = ((String*)obj)->length; - return VAR_NUM((double)length); + String* str = (String*)obj; + SWITCH_ATTRIB(attrib->data) { + + CASE_ATTRIB("length", 0x83d03615) : + return VAR_NUM((double)(str->length)); + + CASE_ATTRIB("lower", 0xb51d04ba) : + return VAR_OBJ(stringLower(vm, str)); + + CASE_ATTRIB("upper", 0xa8c6a47) : + return VAR_OBJ(stringUpper(vm, str)); + + CASE_ATTRIB("strip", 0xfd1b18d1) : + return VAR_OBJ(stringStrip(vm, str)); + + default: + ERR_NO_ATTRIB(on); + return VAR_NULL; } - ERR_NO_ATTRIB(); - return VAR_NULL; + UNREACHABLE(); } case OBJ_LIST: { - if (IS_ATTRIB("length")) { - size_t length = ((List*)obj)->elements.count; - return VAR_NUM((double)length); + List* list = (List*)obj; + SWITCH_ATTRIB(attrib->data) { + + CASE_ATTRIB("length", 0x83d03615) : + return VAR_NUM((double)(list->elements.count)); + + default: + ERR_NO_ATTRIB(on); + return VAR_NULL; } - ERR_NO_ATTRIB(); - return VAR_NULL; + UNREACHABLE(); } case OBJ_MAP: { - TODO; // Not sure should I allow this(below). - //Var value = mapGet((Map*)obj, VAR_OBJ(attrib)); - //if (IS_UNDEF(value)) { - // vm->fiber->error = stringFormat(vm, "Key (\"@\") not exists.", - // attrib); - // return VAR_NULL; - //} - //return value; + // Not sure should I allow string values could be accessed with + // this way. ex: + // map = { "foo" : 42, "can't access" : 32 } + // val = map.foo ## 42 + TODO; } case OBJ_RANGE: { Range* range = (Range*)obj; + SWITCH_ATTRIB(attrib->data) { - if (IS_ATTRIB("as_list")) { - List* list; - if (range->from < range->to) { - list = newList(vm, (uint32_t)(range->to - range->from)); - for (double i = range->from; i < range->to; i++) { - pkVarBufferWrite(&list->elements, vm, VAR_NUM(i)); - } - } else { - list = newList(vm, 0); - } - return VAR_OBJ(list); + CASE_ATTRIB("as_list", 0x1562c22) : + return VAR_OBJ(rangeAsList(vm, range)); + + default: + ERR_NO_ATTRIB(on); + return VAR_NULL; } - ERR_NO_ATTRIB(); - return VAR_NULL; + UNREACHABLE(); } case OBJ_SCRIPT: { @@ -1114,11 +1156,28 @@ Var varGetAttrib(PKVM* vm, Var on, String* attrib) { return scr->globals.data[index]; } - ERR_NO_ATTRIB(); + ERR_NO_ATTRIB(on); return VAR_NULL; } case OBJ_FUNC: + { + Function* fn = (Function*)obj; + SWITCH_ATTRIB(attrib->data) { + + CASE_ATTRIB("arity", 0x3e96bd7a) : + return VAR_NUM((double)(fn->arity)); + + CASE_ATTRIB("name", 0x8d39bde6) : + return VAR_OBJ(newString(vm, fn->name)); + + default: + ERR_NO_ATTRIB(on); + return VAR_NULL; + } + UNREACHABLE(); + } + case OBJ_FIBER: case OBJ_USER: TODO; @@ -1133,10 +1192,10 @@ Var varGetAttrib(PKVM* vm, Var on, String* attrib) { void varSetAttrib(PKVM* vm, Var on, String* attrib, Var value) { -#define ATTRIB_IMMUTABLE(prop) \ +#define ATTRIB_IMMUTABLE(name) \ do { \ - if (IS_ATTRIB(prop)) { \ - vm->fiber->error = stringFormat(vm, "'$' attribute is immutable.", prop); \ + if ((attrib->length == strlen(name) && strcmp(name, attrib->data) == 0)) { \ + vm->fiber->error = stringFormat(vm, "'$' attribute is immutable.", name); \ return; \ } \ } while (false) @@ -1151,21 +1210,30 @@ do { \ switch (obj->type) { case OBJ_STRING: ATTRIB_IMMUTABLE("length"); - ERR_NO_ATTRIB(); + ATTRIB_IMMUTABLE("lower"); + ATTRIB_IMMUTABLE("upper"); + ATTRIB_IMMUTABLE("strip"); + ERR_NO_ATTRIB(on); return; case OBJ_LIST: ATTRIB_IMMUTABLE("length"); - ERR_NO_ATTRIB(); + ERR_NO_ATTRIB(on); return; case OBJ_MAP: + // Not sure should I allow string values could be accessed with + // this way. ex: + // map = { "foo" : 42, "can't access" : 32 } + // map.foo = 'bar' TODO; - ERR_NO_ATTRIB(); + + ERR_NO_ATTRIB(on); return; case OBJ_RANGE: - ERR_NO_ATTRIB(); + ATTRIB_IMMUTABLE("as_list"); + ERR_NO_ATTRIB(on); return; case OBJ_SCRIPT: { @@ -1187,20 +1255,22 @@ do { \ return; } - ERR_NO_ATTRIB(); + ERR_NO_ATTRIB(on); return; } case OBJ_FUNC: - ERR_NO_ATTRIB(); + ATTRIB_IMMUTABLE("arity"); + ATTRIB_IMMUTABLE("name"); + ERR_NO_ATTRIB(on); return; case OBJ_FIBER: - ERR_NO_ATTRIB(); + ERR_NO_ATTRIB(on); return; case OBJ_USER: - TODO; //ERR_NO_ATTRIB(); + TODO; //ERR_NO_ATTRIB(on); return; default: @@ -1208,6 +1278,8 @@ do { \ } CHECK_MISSING_OBJ_TYPE(7); UNREACHABLE(); + +#undef ATTRIB_IMMUTABLE } Var varGetSubscript(PKVM* vm, Var on, Var key) { @@ -1221,7 +1293,7 @@ Var varGetSubscript(PKVM* vm, Var on, Var key) { switch (obj->type) { case OBJ_STRING: { - int32_t index; + int64_t index; String* str = ((String*)obj); if (!validateInteger(vm, key, &index, "List index")) { return VAR_NULL; @@ -1235,12 +1307,12 @@ Var varGetSubscript(PKVM* vm, Var on, Var key) { case OBJ_LIST: { - int32_t index; + int64_t index; pkVarBuffer* elems = &((List*)obj)->elements; if (!validateInteger(vm, key, &index, "List index")) { return VAR_NULL; } - if (!validateIndex(vm, index, (int)elems->count, "List")) { + if (!validateIndex(vm, index, elems->count, "List")) { return VAR_NULL; } return elems->data[index]; @@ -1293,10 +1365,10 @@ void varsetSubscript(PKVM* vm, Var on, Var key, Var value) { case OBJ_LIST: { - int32_t index; + int64_t index; pkVarBuffer* elems = &((List*)obj)->elements; if (!validateInteger(vm, key, &index, "List index")) return; - if (!validateIndex(vm, index, (int)elems->count, "List")) return; + if (!validateIndex(vm, index, elems->count, "List")) return; elems->data[index] = value; return; } diff --git a/src/pk_debug.c b/src/pk_debug.c index a312f58..0ba578d 100644 --- a/src/pk_debug.c +++ b/src/pk_debug.c @@ -185,7 +185,6 @@ void dumpFunctionCode(PKVM* vm, Function* func) { case OP_LIST_APPEND: NO_ARGS(); break; case OP_MAP_INSERT: NO_ARGS(); break; - case OP_PUSH_LOCAL_0: case OP_PUSH_LOCAL_1: case OP_PUSH_LOCAL_2: diff --git a/src/pk_opcodes.h b/src/pk_opcodes.h index e230f4d..9736dea 100644 --- a/src/pk_opcodes.h +++ b/src/pk_opcodes.h @@ -153,7 +153,7 @@ OPCODE(SET_ATTRIB, 2, -1) // Pop var, key, get value and push the result. OPCODE(GET_SUBSCRIPT, 0, -1) -// Get subscript to perform assignment operation before store it, so it won't +// Get subscript to perform assignment operation before store it, so it won't // pop the var and the key. (ex: map[key] += value). OPCODE(GET_SUBSCRIPT_KEEP, 0, 1) diff --git a/src/pk_var.c b/src/pk_var.c index de3407b..d59946a 100644 --- a/src/pk_var.c +++ b/src/pk_var.c @@ -6,6 +6,8 @@ #include "pk_var.h" #include +#include + #include "pk_utils.h" #include "pk_vm.h" @@ -424,6 +426,97 @@ Fiber* newFiber(PKVM* vm, Function* fn) { return fiber; } +List* rangeAsList(PKVM* vm, Range* self) { + List* list; + if (self->from < self->to) { + list = newList(vm, (uint32_t)(self->to - self->from)); + for (double i = self->from; i < self->to; i++) { + pkVarBufferWrite(&list->elements, vm, VAR_NUM(i)); + } + return list; + + } else { + list = newList(vm, 0); + } + + return list; +} + +String* stringLower(PKVM* vm, String* self) { + // If the string itself is already lower don't allocate new string. + uint32_t index = 0; + for (const char* c = self->data; *c != '\0'; c++, index++) { + if (isupper(*c)) { + + // It contain upper case letters, allocate new lower case string . + String* lower = newStringLength(vm, self->data, self->length); + + // Start where the first upper case letter found. + char* _c = lower->data + (c - self->data); + for (; *_c != '\0'; _c++) *_c = (char)tolower(*_c); + + // Since the string is modified re-hash it. + lower->hash = utilHashString(lower->data); + return lower; + } + } + // If we reached here the string itself is lower, return it. + return self; +} + +String* stringUpper(PKVM* vm, String* self) { + // If the string itself is already upper don't allocate new string. + uint32_t index = 0; + for (const char* c = self->data; *c != '\0'; c++, index++) { + if (islower(*c)) { + // It contain lower case letters, allocate new upper case string . + String* upper = newStringLength(vm, self->data, self->length); + + // Start where the first lower case letter found. + char* _c = upper->data + (c - self->data); + for (; *_c != '\0'; _c++) *_c = (char)toupper(*_c); + + // Since the string is modified re-hash it. + upper->hash = utilHashString(upper->data); + return upper; + } + } + // If we reached here the string itself is lower, return it. + return self; +} + +String* stringStrip(PKVM* vm, String* self) { + + // Implementation: + // + // " a string with leading and trailing white space " + // ^start >> << end^ + // + // These 'start' and 'end' pointers will move respectively right and left + // while it's a white space and return an allocated string from 'start' with + // length of (end - start + 1). For already trimed string it'll not allocate + // a new string, instead returns the same string provided. + + const char* start = self->data; + while (*start && isspace(*start)) start++; + + // If we reached the end of the string, it's all white space, return + // an empty string. + if (*start == '\0') { + return newStringLength(vm, NULL, 0); + } + + const char* end = self->data + self->length - 1; + while (isspace(*end)) end--; + + // If the string is already trimed, return the same string. + if (start == self->data && end == self->data + self->length - 1) { + return self; + } + + return newStringLength(vm, start, (uint32_t)(end - start + 1)); +} + void listInsert(PKVM* vm, List* self, uint32_t index, Var value) { // Add an empty slot at the end of the buffer. diff --git a/src/pk_var.h b/src/pk_var.h index 83e184b..c30cb92 100644 --- a/src/pk_var.h +++ b/src/pk_var.h @@ -156,7 +156,6 @@ // TODO: Union tagging implementation of all the above macros ignore macros // starts with an underscore. - typedef enum { VAR_UNDEFINED, //< Internal type for exceptions. VAR_NULL, //< Null pointer type. @@ -450,6 +449,23 @@ void grayFunctionBuffer(PKVM* vm, pkFunctionBuffer* self); // working list to traverse and update the vm's [bytes_allocated] value. void blackenObjects(PKVM* vm); +// Returns a number list from the range. starts with range.from and ends with +// (range.to - 1) increase by 1. Note that if the range is reversed +// (ie. range.from > range.to) It'll return an empty list ([]). +List* rangeAsList(PKVM* vm, Range* self); + +// Returns a lower case version of the given string. If the string is +// already lower it'll return the same string. +String* stringLower(PKVM* vm, String* self); + +// Returns a upper case version of the given string. If the string is +// already upper it'll return the same string. +String* stringUpper(PKVM* vm, String* self); + +// Returns string with the leading and trailing white spaces are trimed. +// If the string is already trimed it'll return the same string. +String* stringStrip(PKVM* vm, String* self); + // Insert [value] to the list at [index] and shift down the rest of the // elements. void listInsert(PKVM* vm, List* self, uint32_t index, Var value); diff --git a/test/run.py b/test/run.py deleted file mode 100644 index 06ed6e6..0000000 --- a/test/run.py +++ /dev/null @@ -1,87 +0,0 @@ -import subprocess, os, sys -import json, re -from os.path import join - -## All the test files. -test_files = [ - "lang/basics.pk", - "lang/functions.pk", - "lang/controlflow.pk", - "lang/fibers.pk", - "lang/import.pk", - - "examples/helloworld.pk", - "examples/brainfuck.pk", - "examples/fib.pk", - "examples/prime.pk", - "examples/fizzbuzz.pk", - "examples/pi.pk", -] - -## All benchmark files. ## TODO: pass args for iterations. -benchmarks = { - "factors" : ['.pk', '.py', '.rb', '.wren'], - "fib" : ['.pk', '.py', '.rb', '.wren'], - "list" : ['.pk', '.py', '.rb'], - "loop" : ['.pk', '.py', '.rb', ".wren"], - "primes" : ['.pk', '.py', '.rb', ".wren"], -} - -def main(): - run_all_tests() - run_all_benchmarks() - - -def run_all_benchmarks(): - print_title("BENCHMARKS") - - def get_interpreter(file): - if file.endswith('.pk' ) : return 'pocket' - if file.endswith('.py' ) : return 'python' - if file.endswith('.rb' ) : return 'ruby' - if file.endswith('.wren') : return 'wren' - assert False - - for bm_name in benchmarks: - print(bm_name + ":") - for ext in benchmarks[bm_name]: - file = join('benchmark', bm_name, bm_name + ext) - interpreter = get_interpreter(file) - print(' %10s: '%interpreter, end=''); sys.stdout.flush() - result = run_command([interpreter, file]) - time = re.findall(r'elapsed:\s*([0-9\.]+)\s*s', - result.stdout.decode('utf8'), - re.MULTILINE) - assert len(time) == 1, r'elapsed:\s*([0-9\.]+)\s*s --> no mach found.' - print('%10ss'%time[0]) - -def run_all_tests(): - print_title("TESTS") - - FMT_PATH = "%-25s" - INDENTATION = ' | ' - for path in test_files: - print(FMT_PATH % path, end='') - result = run_command(['pocket', path]) - if result.returncode != 0: - print('-- Failed') - err = INDENTATION + result.stderr \ - .decode('utf8') \ - .replace('\n', '\n' + INDENTATION) - print(err) - else: - print('-- OK') - - -def run_command(command): - return subprocess.run(command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - -def print_title(title): - print("--------------------------------") - print(" %s " % title) - print("--------------------------------") - -if __name__ == '__main__': - main() diff --git a/test/benchmark/factors/factors.js b/tests/benchmark/factors/factors.js similarity index 100% rename from test/benchmark/factors/factors.js rename to tests/benchmark/factors/factors.js diff --git a/test/benchmark/factors/factors.pk b/tests/benchmark/factors/factors.pk similarity index 100% rename from test/benchmark/factors/factors.pk rename to tests/benchmark/factors/factors.pk diff --git a/test/benchmark/factors/factors.py b/tests/benchmark/factors/factors.py similarity index 100% rename from test/benchmark/factors/factors.py rename to tests/benchmark/factors/factors.py diff --git a/test/benchmark/factors/factors.rb b/tests/benchmark/factors/factors.rb similarity index 100% rename from test/benchmark/factors/factors.rb rename to tests/benchmark/factors/factors.rb diff --git a/test/benchmark/factors/factors.wren b/tests/benchmark/factors/factors.wren similarity index 100% rename from test/benchmark/factors/factors.wren rename to tests/benchmark/factors/factors.wren diff --git a/test/benchmark/fib/fib.pk b/tests/benchmark/fib/fib.pk similarity index 100% rename from test/benchmark/fib/fib.pk rename to tests/benchmark/fib/fib.pk diff --git a/test/benchmark/fib/fib.py b/tests/benchmark/fib/fib.py similarity index 100% rename from test/benchmark/fib/fib.py rename to tests/benchmark/fib/fib.py diff --git a/test/benchmark/fib/fib.rb b/tests/benchmark/fib/fib.rb similarity index 100% rename from test/benchmark/fib/fib.rb rename to tests/benchmark/fib/fib.rb diff --git a/test/benchmark/fib/fib.wren b/tests/benchmark/fib/fib.wren similarity index 100% rename from test/benchmark/fib/fib.wren rename to tests/benchmark/fib/fib.wren diff --git a/test/benchmark/list/list.pk b/tests/benchmark/list/list.pk similarity index 100% rename from test/benchmark/list/list.pk rename to tests/benchmark/list/list.pk diff --git a/test/benchmark/list/list.py b/tests/benchmark/list/list.py similarity index 100% rename from test/benchmark/list/list.py rename to tests/benchmark/list/list.py diff --git a/test/benchmark/list/list.rb b/tests/benchmark/list/list.rb similarity index 100% rename from test/benchmark/list/list.rb rename to tests/benchmark/list/list.rb diff --git a/test/benchmark/loop/loop.js b/tests/benchmark/loop/loop.js similarity index 100% rename from test/benchmark/loop/loop.js rename to tests/benchmark/loop/loop.js diff --git a/test/benchmark/loop/loop.pk b/tests/benchmark/loop/loop.pk similarity index 100% rename from test/benchmark/loop/loop.pk rename to tests/benchmark/loop/loop.pk diff --git a/test/benchmark/loop/loop.py b/tests/benchmark/loop/loop.py similarity index 100% rename from test/benchmark/loop/loop.py rename to tests/benchmark/loop/loop.py diff --git a/test/benchmark/loop/loop.rb b/tests/benchmark/loop/loop.rb similarity index 100% rename from test/benchmark/loop/loop.rb rename to tests/benchmark/loop/loop.rb diff --git a/test/benchmark/loop/loop.wren b/tests/benchmark/loop/loop.wren similarity index 100% rename from test/benchmark/loop/loop.wren rename to tests/benchmark/loop/loop.wren diff --git a/test/benchmark/primes/primes.pk b/tests/benchmark/primes/primes.pk similarity index 100% rename from test/benchmark/primes/primes.pk rename to tests/benchmark/primes/primes.pk diff --git a/test/benchmark/primes/primes.py b/tests/benchmark/primes/primes.py similarity index 100% rename from test/benchmark/primes/primes.py rename to tests/benchmark/primes/primes.py diff --git a/test/benchmark/primes/primes.rb b/tests/benchmark/primes/primes.rb similarity index 100% rename from test/benchmark/primes/primes.rb rename to tests/benchmark/primes/primes.rb diff --git a/test/benchmark/primes/primes.wren b/tests/benchmark/primes/primes.wren similarity index 100% rename from test/benchmark/primes/primes.wren rename to tests/benchmark/primes/primes.wren diff --git a/tests/check.py b/tests/check.py new file mode 100644 index 0000000..ffca761 --- /dev/null +++ b/tests/check.py @@ -0,0 +1,127 @@ +#!python +## Copyright (c) 2020-2021 Thakee Nathees +## Distributed Under The MIT License + +## This will run a static checks on the source files, for line length, +## uses of tabs and trailing white spaces, etc. +## +## Run the file at the top level of the repository "python3 tests/check.py". + +import os, sys, re +from os.path import join +from os import listdir + +## A list of source files, to check if the fnv1a hash values match it's +## corresponding cstring in the CASE_ATTRIB(name, hash) macro calls. +HASH_CHECK_LIST = [ + "src/pk_core.c", +] + +## A list of directory, contains C source files to perform static checks. +## This will include both '.c' and '.h' files. +C_SOURCE_DIRS = [ + "src/", + "cli/", + "cli/modules/", +] + +## This global variable will be set to true if any check failed. +checks_failed = False + +def main(): + dir = os.path.dirname(os.path.realpath(__file__)) + if dir == os.getcwd(): + print("Run the file from the top level of the repository", file=sys.stderr) + sys.exit(1) + + check_fnv1_hash(HASH_CHECK_LIST) + check_static(C_SOURCE_DIRS) + if checks_failed: + sys.exit(1) + print("Static checks were passed.") + +def check_fnv1_hash(sources): + PATTERN = r'CASE_ATTRIB\(\s*"([A-Za-z0-9_]+)"\s*,\s*(0x[0-9abcdef]+)\)' + for file in sources: + fp = open(file, 'r') + + line_no = 0 + for line in fp.readlines(): + line_no += 1 + match = re.findall(PATTERN, line) + if len(match) == 0: continue + name, val = match[0] + hash = hex(fnv1a_hash(name)) + + if val == hash: continue + report_error(f"{location(file, line_no)} - hash mismatch. " + f"hash('{name}') = {hash} not {val}") + + fp.close() + +## Check each source file ('.c', '.h', '.py') in the [dirs] contains tabs, +## more than 79 characters and trailing white space. +def check_static(dirs): + valid_ext = ('.c', '.h', '.py', '.pk') + for dir in dirs: + + for file in listdir(dir): + if not file.endswith(valid_ext): continue + if os.path.isdir(join(dir, file)): continue + + fp = open(join(dir, file), 'r') + + ## This will be set to true if the last line is empty. + is_last_empty = False; line_no = 0 + for line in fp.readlines(): + line_no += 1; line = line[:-1] # remove the line ending. + + _location = location(join(dir, file), line_no) + + ## Check if the line contains any tabs. + if '\t' in line: + report_error(f"{_location} - contains tab(s) ({repr(line)}).") + + if len(line) >= 80: + if 'http://' in line or 'https://' in line: continue + report_error( + f"{_location} - contains {len(line)} (> 79) characters.") + + if line.endswith(' '): + report_error(f"{_location} - contains trailing white space.") + + if line == '': + if is_last_empty: + report_error(f"{_location} - consequent empty lines.") + is_last_empty = True + else: + is_last_empty = False + + fp.close() + + +## Returns a formated string of the error location. +def location(file, line): + return f"{'%-17s'%file} : {'%4s'%line}" + +## FNV-1a hash. See: http://www.isthe.com/chongo/tech/comp/fnv/ +def fnv1a_hash(string): + FNV_prime_32_bit = 16777619 + FNV_offset_basis_32_bit = 2166136261 + + hash = FNV_offset_basis_32_bit + + for c in string: + hash ^= ord(c) + hash *= FNV_prime_32_bit + hash &= 0xffffffff ## intentional 32 bit overflow. + return hash + +def report_error(msg): + global checks_failed + checks_failed = True + print(msg, file=sys.stderr) + +if __name__ == '__main__': + main() + diff --git a/test/examples/brainfuck.pk b/tests/examples/brainfuck.pk similarity index 100% rename from test/examples/brainfuck.pk rename to tests/examples/brainfuck.pk diff --git a/test/examples/fib.pk b/tests/examples/fib.pk similarity index 100% rename from test/examples/fib.pk rename to tests/examples/fib.pk diff --git a/test/examples/fizzbuzz.pk b/tests/examples/fizzbuzz.pk similarity index 100% rename from test/examples/fizzbuzz.pk rename to tests/examples/fizzbuzz.pk diff --git a/test/examples/helloworld.pk b/tests/examples/helloworld.pk similarity index 100% rename from test/examples/helloworld.pk rename to tests/examples/helloworld.pk diff --git a/test/examples/palette.pk b/tests/examples/palette.pk similarity index 100% rename from test/examples/palette.pk rename to tests/examples/palette.pk diff --git a/test/examples/pi.pk b/tests/examples/pi.pk similarity index 100% rename from test/examples/pi.pk rename to tests/examples/pi.pk diff --git a/test/examples/prime.pk b/tests/examples/prime.pk similarity index 100% rename from test/examples/prime.pk rename to tests/examples/prime.pk diff --git a/test/lang/basics.pk b/tests/lang/basics.pk similarity index 100% rename from test/lang/basics.pk rename to tests/lang/basics.pk diff --git a/test/lang/controlflow.pk b/tests/lang/controlflow.pk similarity index 100% rename from test/lang/controlflow.pk rename to tests/lang/controlflow.pk diff --git a/tests/lang/core.pk b/tests/lang/core.pk new file mode 100644 index 0000000..f62d35f --- /dev/null +++ b/tests/lang/core.pk @@ -0,0 +1,32 @@ + +## Core builtin functions and attribute tests. + +assert(hex(12648430) == '0xc0ffee') +assert(hex(255) == '0xff' and hex(10597059) == '0xa1b2c3') +assert(hex(-4294967295) == '-0xffffffff') ## the largest. + +## string attributes. +assert(''.length == 0) +assert('test'.length == 4) +assert(''.lower == '' and ''.upper == '') +assert('already+lower '.lower == 'already+lower ') +assert('ALREADY+UPPER '.upper == 'ALREADY+UPPER ') +assert('tEST+InG'.lower == 'test+ing') +assert('tEST+InG'.upper == 'TEST+ING') + +assert(' trim '.strip == 'trim') +assert(''.strip == '') + +## List attribute +assert([].length == 0) +assert([1, 2, 3].length == 3) + +## Function +assert(print.arity == -1) +assert(hex.arity == 1) +assert(func(a, b)end .arity == 2) +assert(print.name == "print") +def fn(p1, p2, p3) end +assert(fn.name == "fn") +assert(fn.arity == 3) + diff --git a/test/lang/fibers.pk b/tests/lang/fibers.pk similarity index 100% rename from test/lang/fibers.pk rename to tests/lang/fibers.pk diff --git a/test/lang/functions.pk b/tests/lang/functions.pk similarity index 82% rename from test/lang/functions.pk rename to tests/lang/functions.pk index b5f1a1f..c36d105 100644 --- a/test/lang/functions.pk +++ b/tests/lang/functions.pk @@ -22,5 +22,6 @@ def fn3(data) return '[fn3:' + data + ']' end result = 'data' -> fn1 -> fn2{'suff'} -> fn3 assert(result == '[fn3:[fn2:[fn1:data]|suff]]') -result = ' tEST+InG ' -> str_strip -> str_lower -assert(result == 'test+ing') +# str_lower(s) function refactored to s.lower attribute. +#result = ' tEST+InG ' -> str_strip -> str_lower +#assert(result == 'test+ing') diff --git a/test/lang/import.pk b/tests/lang/import.pk similarity index 100% rename from test/lang/import.pk rename to tests/lang/import.pk diff --git a/test/lang/import/all_import.pk b/tests/lang/import/all_import.pk similarity index 100% rename from test/lang/import/all_import.pk rename to tests/lang/import/all_import.pk diff --git a/test/lang/import/module.pk b/tests/lang/import/module.pk similarity index 100% rename from test/lang/import/module.pk rename to tests/lang/import/module.pk diff --git a/tests/tests.py b/tests/tests.py new file mode 100644 index 0000000..0a80a87 --- /dev/null +++ b/tests/tests.py @@ -0,0 +1,90 @@ +import subprocess, os, sys +import json, re +from os.path import join + +## TODO: Re write this in doctest (https://github.com/onqtam/doctest) + +## All the test files. +test_files = [ + "lang/basics.pk", + "lang/core.pk", + "lang/controlflow.pk", + "lang/fibers.pk", + "lang/functions.pk", + "lang/import.pk", + + "examples/brainfuck.pk", + "examples/fib.pk", + "examples/fizzbuzz.pk", + "examples/helloworld.pk", + "examples/pi.pk", + "examples/prime.pk", + +] + +## All benchmark files. ## TODO: pass args for iterations. +benchmarks = { + "factors" : ['.pk', '.py', '.rb', '.wren'], + "fib" : ['.pk', '.py', '.rb', '.wren'], + "list" : ['.pk', '.py', '.rb'], + "loop" : ['.pk', '.py', '.rb', ".wren"], + "primes" : ['.pk', '.py', '.rb', ".wren"], +} + +def main(): + run_all_tests() + #run_all_benchmarks() + +def run_all_benchmarks(): + print_title("BENCHMARKS") + + def get_interpreter(file): + if file.endswith('.pk' ) : return 'pocket' + if file.endswith('.py' ) : return 'python' + if file.endswith('.rb' ) : return 'ruby' + if file.endswith('.wren') : return 'wren' + assert False + + for bm_name in benchmarks: + print(bm_name + ":") + for ext in benchmarks[bm_name]: + file = join('benchmark', bm_name, bm_name + ext) + interpreter = get_interpreter(file) + print(' %10s: '%interpreter, end=''); sys.stdout.flush() + result = run_command([interpreter, file]) + time = re.findall(r'elapsed:\s*([0-9\.]+)\s*s', + result.stdout.decode('utf8'), + re.MULTILINE) + assert len(time) == 1, r'elapsed:\s*([0-9\.]+)\s*s --> no mach found.' + print('%10ss'%time[0]) + +def run_all_tests(): + print_title("TESTS") + + FMT_PATH = "%-25s" + INDENTATION = ' | ' + for path in test_files: + print(FMT_PATH % path, end='') + result = run_command(['pocket', path]) + if result.returncode != 0: + print('-- Failed') + err = INDENTATION + result.stderr \ + .decode('utf8') \ + .replace('\n', '\n' + INDENTATION) + print(err) + else: + print('-- OK') + + +def run_command(command): + return subprocess.run(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + +def print_title(title): + print("--------------------------------") + print(" %s " % title) + print("--------------------------------") + +if __name__ == '__main__': + main()