diff --git a/CMakeLists.txt b/CMakeLists.txt index 2aae71f4..6c0c767b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ cmake_policy(SET CMP0079 NEW) option(DUSK_BUILD_TESTS "Enable tests" OFF) option(DUSK_NETWORK "Enable network support" ON) +option(DUSK_SCRIPT "Enable scripting support" ON) set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name") set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder") @@ -110,6 +111,12 @@ if(DUSK_NETWORK) ) endif() +if(DUSK_SCRIPT) + target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + DUSK_SCRIPT + ) +endif() + # Toolchains include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f5cd6de..59bbb0fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,16 @@ if(DUSK_NETWORK) add_subdirectory(dusknetwork) endif() +if( + DUSK_SCRIPT AND + (DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") +) + # The duskscript runtime itself lives in src/dusk/script, added + # unconditionally of platform there since it is plain, portable C; this is + # just the duskscriptcc compiler, a host tool only built on the dev machine. + add_subdirectory(duskscriptcc) +endif() + if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") add_subdirectory(dusklinux) add_subdirectory(dusksdl2) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index c0347f89..228f966b 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -82,6 +82,9 @@ add_subdirectory(input) add_subdirectory(locale) add_subdirectory(perf) add_subdirectory(rpg) +if(DUSK_SCRIPT) + add_subdirectory(script) +endif() add_subdirectory(scene) add_subdirectory(system) add_subdirectory(time) diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index f1420c4a..d7a6e3f4 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -20,6 +20,9 @@ #ifdef DUSK_NETWORK #include "network/network.h" #endif +#ifdef DUSK_SCRIPT + #include "script/script.h" +#endif #include "system/system.h" #include "console/console.h" #include "save/save.h" @@ -52,6 +55,9 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { #ifdef DUSK_NETWORK errorChain(networkInit()); #endif + #ifdef DUSK_SCRIPT + errorChain(scriptInit()); + #endif errorChain(sceneInit()); consolePrint("Engine initialized"); @@ -95,6 +101,9 @@ void engineExit(void) { errorret_t engineDispose(void) { errorChain(sceneDispose()); + #ifdef DUSK_SCRIPT + errorChain(scriptDispose()); + #endif #ifdef DUSK_NETWORK errorChain(networkDispose()); #endif diff --git a/src/dusk/script/CMakeLists.txt b/src/dusk/script/CMakeLists.txt new file mode 100644 index 00000000..69580e52 --- /dev/null +++ b/src/dusk/script/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Includes. core/ and vm/ are their own include roots: their own headers +# include each other as "value/scriptvalue.h", "execute/scriptvmcode.h" and so +# on, with no "script/" prefix, the same as duskscriptcc's copy of core/. +target_include_directories(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/core + ${CMAKE_CURRENT_LIST_DIR}/vm +) + +target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC m) + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + script.c + scriptnativeconsole.c +) + +# Subdirs +add_subdirectory(core) +add_subdirectory(vm) diff --git a/src/dusk/script/core/CMakeLists.txt b/src/dusk/script/core/CMakeLists.txt new file mode 100644 index 00000000..de2bcba4 --- /dev/null +++ b/src/dusk/script/core/CMakeLists.txt @@ -0,0 +1,89 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + array/scriptarraycopy.c + array/scriptarraydeep.c + array/scriptarraymodify.c + array/scriptarraynew.c + array/scriptarraysort.c + builtin/array/scriptbarraybasic.c + builtin/array/scriptbarrayfunc.c + builtin/array/scriptbarraymodify.c + builtin/array/scriptbarrayorder.c + builtin/array/scriptbarraysearch.c + builtin/array/scriptbarrayslice.c + builtin/core/scriptbconvert.c + builtin/core/scriptbinclude.c + builtin/core/scriptbprint.c + builtin/core/scriptbpromise.c + builtin/core/scriptbtype.c + builtin/math/scriptbmathbasic.c + builtin/math/scriptbmathrandom.c + builtin/math/scriptbmathround.c + builtin/math/scriptbmathtrig.c + builtin/scriptarg.c + builtin/scriptbuiltinregister.c + builtin/scriptintrinsic.c + builtin/scriptnativesig.c + builtin/string/scriptbstringcase.c + builtin/string/scriptbstringsearch.c + builtin/string/scriptbstringsplit.c + builtin/string/scriptbstringtrim.c + bytecode/scriptbytecodesize.c + bytecode/scriptvarint.c + call/scriptcall.c + coroutine/scriptcoroutineready.c + coroutine/scriptcoroutinespawn.c + coroutine/scriptframelocate.c + coroutine/scriptscheduler.c + coroutine/scriptsetposition.c + coroutine/scriptstackcheck.c + error/scriptraise.c + error/scripttrace.c + gc/scriptgccollect.c + gc/scriptgcmark.c + gc/scriptgcscan.c + heap/scriptheapalloc.c + heap/scriptheapblock.c + heap/scriptheapfree.c + heap/scriptheapinit.c + heap/scriptheapsweep.c + number/scriptnumberbinary.c + number/scriptnumberformat.c + number/scriptnumberparse.c + number/scriptnumberunary.c + ops/scriptbinaryop.c + ops/scriptindex.c + ops/scriptref.c + promise/scriptpromiseawait.c + promise/scriptpromisecombine.c + promise/scriptpromisenew.c + promise/scriptpromisesettle.c + promise/scripttimer.c + runtime/scriptruntimeconfig.c + runtime/scriptruntimecreate.c + runtime/scriptruntimeexecute.c + runtime/scriptruntimeload.c + runtime/scriptruntimerun.c + scope/scriptscopefind.c + scope/scriptscopenew.c + scope/scriptscopeset.c + scope/scriptvariable.c + string/scriptbuf.c + string/scriptstringchar.c + string/scriptstringconcat.c + string/scriptstringnew.c + string/scriptstringreject.c + string/scriptstringsearch.c + struct/scriptstruct.c + text/scripttext.c + value/scriptalloc.c + value/scriptfunction.c + value/scriptvaluecompare.c + value/scriptvalueformat.c + value/scriptvalueprimitive.c +) diff --git a/src/dusk/script/core/array/scriptarraycopy.c b/src/dusk/script/core/array/scriptarraycopy.c new file mode 100644 index 00000000..000c12ab --- /dev/null +++ b/src/dusk/script/core/array/scriptarraycopy.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "array/scriptarraycopy.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "value/scriptfunction.h" + +scriptvalue_t scriptArrayCopy(scriptruntime_t *runtime, const scriptarray_t *source) { + scriptvalue_t result = scriptArrayNew(runtime); + uint16_t i; + for(i = 0; i < source->length; i++) scriptArrayPushRaw(runtime, result.as.array, source->items[i]); + result.as.array->category = source->category; + return result; +} diff --git a/src/dusk/script/core/array/scriptarraycopy.h b/src/dusk/script/core/array/scriptarraycopy.h new file mode 100644 index 00000000..30e11c2d --- /dev/null +++ b/src/dusk/script/core/array/scriptarraycopy.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ARRAY_SCRIPTARRAYCOPY_H +#define SCRIPT_ARRAY_SCRIPTARRAYCOPY_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +/** + * Copies the contents of one array into a new array with the same category. + * + * @param runtime The runtime. + * @param source The array to copy. + * @returns The new array value. + */ +scriptvalue_t scriptArrayCopy(scriptruntime_t *runtime, const scriptarray_t *source); + +#endif diff --git a/src/dusk/script/core/array/scriptarraydeep.c b/src/dusk/script/core/array/scriptarraydeep.c new file mode 100644 index 00000000..7a55d39c --- /dev/null +++ b/src/dusk/script/core/array/scriptarraydeep.c @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "array/scriptarraydeep.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "error/scriptraise.h" +#include "struct/scriptstruct.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" + +#define SCRIPT_DEEP_MAX 16 + +scriptvalue_t scriptDeepCopy(scriptruntime_t *runtime, const scriptvalue_t value, const uint8_t depth) { + scriptvalue_t result; + uint16_t i; + + if(value.type != SCRIPT_VALUE_ARRAY && value.type != SCRIPT_VALUE_STRUCT) return value; + if(depth >= SCRIPT_DEEP_MAX) scriptRaise(runtime, "Too deeply nested to copy, or it contains itself"); + + if(value.type == SCRIPT_VALUE_ARRAY) { + result = scriptArrayNew(runtime); + for(i = 0; i < value.as.array->length; i++) { + scriptArrayPushRaw(runtime, result.as.array, scriptDeepCopy(runtime, value.as.array->items[i], depth + 1)); + } + result.as.array->category = value.as.array->category; + return result; + } + + result = scriptStructNew(runtime); + for(i = 0; i < value.as.structure->count; i++) { + scriptStructSet( + runtime, + result.as.structure, + value.as.structure->keys[i]->data, + value.as.structure->keys[i]->length, + scriptDeepCopy(runtime, value.as.structure->values[i], depth + 1) + ); + } + return result; +} + +bool scriptDeepEqual(const scriptvalue_t a, const scriptvalue_t b, const uint8_t depth) { + uint16_t i; + int slot; + + if(depth >= SCRIPT_DEEP_MAX) return false; + + if(a.type == SCRIPT_VALUE_ARRAY && b.type == SCRIPT_VALUE_ARRAY) { + if(a.as.array->length != b.as.array->length) return false; + for(i = 0; i < a.as.array->length; i++) { + if(!scriptDeepEqual(a.as.array->items[i], b.as.array->items[i], depth + 1)) return false; + } + return true; + } + + if(a.type == SCRIPT_VALUE_STRUCT && b.type == SCRIPT_VALUE_STRUCT) { + if(a.as.structure->count != b.as.structure->count) return false; + for(i = 0; i < a.as.structure->count; i++) { + slot = scriptStructFind(b.as.structure, a.as.structure->keys[i]->data, a.as.structure->keys[i]->length); + if(slot < 0) return false; + if(!scriptDeepEqual(a.as.structure->values[i], b.as.structure->values[slot], depth + 1)) return false; + } + return true; + } + + return scriptValuesEqual(a, b); +} diff --git a/src/dusk/script/core/array/scriptarraydeep.h b/src/dusk/script/core/array/scriptarraydeep.h new file mode 100644 index 00000000..afbd4611 --- /dev/null +++ b/src/dusk/script/core/array/scriptarraydeep.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ARRAY_SCRIPTARRAYDEEP_H +#define SCRIPT_ARRAY_SCRIPTARRAYDEEP_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +/** + * Copies arrays and structs all the way down. + * + * @param runtime The runtime. + * @param value The value to copy. + * @param depth How deep the copy already is. + * @returns The copy. + */ +scriptvalue_t scriptDeepCopy(scriptruntime_t *runtime, const scriptvalue_t value, const uint8_t depth); + +/** + * Whether two values are deeply equal: arrays and structs are compared by what + * they hold. + * + * @param a First value. + * @param b Second value. + * @param depth How deep the comparison already is. + * @returns True when they match. + */ +bool scriptDeepEqual(const scriptvalue_t a, const scriptvalue_t b, const uint8_t depth); + +#endif diff --git a/src/dusk/script/core/array/scriptarraymodify.c b/src/dusk/script/core/array/scriptarraymodify.c new file mode 100644 index 00000000..2cd24396 --- /dev/null +++ b/src/dusk/script/core/array/scriptarraymodify.c @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "array/scriptarraymodify.h" +#include "value/scriptalloc.h" +#include "array/scriptarraynew.h" +#include "error/scriptraise.h" +#include "value/scriptfunction.h" +#include + +void scriptArrayPush(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value) { + if(array->frozen) scriptRaise(runtime, "Can not change a constant"); + scriptArrayCheck(runtime, array, value); + scriptArrayPushRaw(runtime, array, value); +} + +void scriptArrayReserve(scriptruntime_t *runtime, scriptarray_t *array, const uint16_t capacity) { + scriptvalue_t *items; + + if(capacity <= array->capacity) return; + if(capacity > SCRIPT_ARRAY_LENGTH_MAX) scriptRaise(runtime, "Arrays hold up to %d elements", SCRIPT_ARRAY_LENGTH_MAX); + items = scriptAllocKind(runtime, sizeof(scriptvalue_t) * capacity, SCRIPT_HEAP_KIND_ITEMS); + if(array->length) memoryCopy(items, array->items, sizeof(scriptvalue_t) * array->length); + scriptFree(runtime, array->items); + array->items = items; + array->capacity = capacity; +} + +void scriptArrayPushRaw(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value) { + if(array->frozen) scriptRaise(runtime, "Can not change a constant"); + if(array->length >= SCRIPT_ARRAY_LENGTH_MAX) { + scriptRaise(runtime, "Arrays hold up to %d elements", SCRIPT_ARRAY_LENGTH_MAX); + } + + if(array->length >= array->capacity) { + uint16_t capacity = array->capacity ? (uint16_t)(array->capacity * 2) : 4; + scriptvalue_t *items; + if(capacity > SCRIPT_ARRAY_LENGTH_MAX) capacity = SCRIPT_ARRAY_LENGTH_MAX; + items = scriptAllocKind(runtime, sizeof(scriptvalue_t) * capacity, SCRIPT_HEAP_KIND_ITEMS); + if(array->length) memoryCopy(items, array->items, sizeof(scriptvalue_t) * array->length); + scriptFree(runtime, array->items); + array->items = items; + array->capacity = capacity; + } + + array->items[array->length++] = value; +} + +void scriptArrayInsert(scriptruntime_t *runtime, scriptarray_t *array, const uint16_t index, const scriptvalue_t value) { + uint16_t i; + scriptArrayCheck(runtime, array, value); + scriptArrayPushRaw(runtime, array, value); + for(i = (uint16_t)(array->length - 1); i > index; i--) { + array->items[i] = array->items[i - 1]; + } + array->items[index] = value; +} + +scriptvalue_t scriptArrayRemove(scriptarray_t *array, const uint16_t index) { + scriptvalue_t removed = array->items[index]; + uint16_t i; + for(i = index; i + 1 < array->length; i++) array->items[i] = array->items[i + 1]; + array->length--; + return removed; +} + +void scriptFreeze(scriptruntime_t *runtime, const scriptvalue_t value) { + uint16_t i; + + if(value.type == SCRIPT_VALUE_ARRAY) { + for(i = 0; i < value.as.array->length; i++) scriptRequireConstant(runtime, value.as.array->items[i]); + value.as.array->frozen = true; + } else if(value.type == SCRIPT_VALUE_STRUCT) { + for(i = 0; i < value.as.structure->count; i++) scriptRequireConstant(runtime, value.as.structure->values[i]); + value.as.structure->frozen = true; + } +} + +void scriptRequireConstant(scriptruntime_t *runtime, const scriptvalue_t value) { + if((value.type == SCRIPT_VALUE_ARRAY && !value.as.array->frozen) || (value.type == SCRIPT_VALUE_STRUCT && !value.as.structure->frozen)) { + scriptRaise(runtime, "A constant can only hold arrays and structs that are constants"); + } +} diff --git a/src/dusk/script/core/array/scriptarraymodify.h b/src/dusk/script/core/array/scriptarraymodify.h new file mode 100644 index 00000000..7b1317f5 --- /dev/null +++ b/src/dusk/script/core/array/scriptarraymodify.h @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ARRAY_SCRIPTARRAYMODIFY_H +#define SCRIPT_ARRAY_SCRIPTARRAYMODIFY_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +/** + * Adds a value on the end of an array, with all the checks. + * + * @param runtime The runtime. + * @param array The array. + * @param value The value to add. + */ +void scriptArrayPush(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value); + +/** + * Makes room in an array for as many items as it is going to have, so adding them does not have to make the room + * again and again. Nothing is done when there is the room. + * + * @param runtime The runtime. + * @param array The array. + * @param capacity How many items. + */ +void scriptArrayReserve(scriptruntime_t *runtime, scriptarray_t *array, const uint16_t capacity); + +/** + * Adds a value on the end without checking its type, for results made by the + * runtime itself. The length limit still applies. + * + * @param runtime The runtime. + * @param array The array. + * @param value The value to add. + */ +void scriptArrayPushRaw(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value); + +/** + * Inserts a value in an array, the index may be the length to add on the end. + * + * @param runtime The runtime. + * @param array The array. + * @param index Where the value goes. + * @param value The value to insert. + */ +void scriptArrayInsert(scriptruntime_t *runtime, scriptarray_t *array, const uint16_t index, const scriptvalue_t value); + +/** + * Removes a value from an array and returns it. + * + * @param array The array. + * @param index The index to remove, must be in bounds. + * @returns The removed value. + */ +scriptvalue_t scriptArrayRemove(scriptarray_t *array, const uint16_t index); + +/** + * Makes an array or a struct a constant, which nothing changes after. What it holds that is an + * array or a struct has to be one already. Anything else is left as it is. + * + * @param runtime The runtime. + * @param value The value. + */ +void scriptFreeze(scriptruntime_t *runtime, const scriptvalue_t value); + +/** + * Fails when a value is an array or a struct that is not a constant. + * + * @param runtime The runtime. + * @param value The value. + */ +void scriptRequireConstant(scriptruntime_t *runtime, const scriptvalue_t value); + +#endif diff --git a/src/dusk/script/core/array/scriptarraynew.c b/src/dusk/script/core/array/scriptarraynew.c new file mode 100644 index 00000000..ca88184c --- /dev/null +++ b/src/dusk/script/core/array/scriptarraynew.c @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "array/scriptarraynew.h" +#include "value/scriptalloc.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" + +scriptvalue_t scriptArrayNew(scriptruntime_t *runtime) { + scriptvalue_t value = { .type = SCRIPT_VALUE_ARRAY }; + value.as.array = scriptAllocKind(runtime, sizeof(scriptarray_t), SCRIPT_HEAP_KIND_ARRAY); + value.as.array->category = SCRIPT_CATEGORY_UNKNOWN; + return value; +} + +scriptcategory_t scriptCategoryOf(const scriptvalue_t value) { + switch(value.type) { + case SCRIPT_VALUE_NUMBER: return SCRIPT_CATEGORY_NUMBER; + case SCRIPT_VALUE_BOOL: return SCRIPT_CATEGORY_BOOL; + case SCRIPT_VALUE_STRING: return SCRIPT_CATEGORY_STRING; + case SCRIPT_VALUE_ARRAY: return SCRIPT_CATEGORY_ARRAY; + case SCRIPT_VALUE_STRUCT: return SCRIPT_CATEGORY_STRUCT; + case SCRIPT_VALUE_FUNCTION: return SCRIPT_CATEGORY_FUNCTION; + case SCRIPT_VALUE_PROMISE: return SCRIPT_CATEGORY_PROMISE; + default: return SCRIPT_CATEGORY_UNKNOWN; + } +} + +void scriptArrayCheck(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value) { + scriptcategory_t category; + + if(value.type == SCRIPT_VALUE_UNDEFINED) { + scriptRaise(runtime, "Arrays can not hold undefined"); + } + + if(value.type == SCRIPT_VALUE_NULL) { + if(array->category == SCRIPT_CATEGORY_NUMBER || array->category == SCRIPT_CATEGORY_BOOL) { + scriptRaise(runtime, "Numbers and bools can not be null, so an array of them can not hold null"); + } + return; + } + + category = scriptCategoryOf(value); + if(array->category == SCRIPT_CATEGORY_UNKNOWN) { + array->category = category; + } else if(array->category != category) { + scriptRaise(runtime, "Arrays can not mix types, tried to add %s to an array of another type", scriptTypeName(value)); + } +} diff --git a/src/dusk/script/core/array/scriptarraynew.h b/src/dusk/script/core/array/scriptarraynew.h new file mode 100644 index 00000000..745e43fc --- /dev/null +++ b/src/dusk/script/core/array/scriptarraynew.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ARRAY_SCRIPTARRAYNEW_H +#define SCRIPT_ARRAY_SCRIPTARRAYNEW_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" +#include "value/scriptarray.h" + +/** + * Makes an empty array that can grow up to the array limit. + * + * @param runtime The runtime. + * @returns The array value. + */ +scriptvalue_t scriptArrayNew(scriptruntime_t *runtime); + +/** + * Which kind of thing an array can hold a value as. + * + * @param value Value to look at. + * @returns The category, SCRIPT_CATEGORY_UNKNOWN for null and undefined. + */ +scriptcategory_t scriptCategoryOf(const scriptvalue_t value); + +/** + * Checks a value can go in an array and sets the array's category from the + * first value placed in it. Mixed types, undefined, and null in an array of + * numbers or bools are fatal errors. + * + * @param runtime The runtime. + * @param array The array. + * @param value The value about to be added. + */ +void scriptArrayCheck(scriptruntime_t *runtime, scriptarray_t *array, const scriptvalue_t value); + +#endif diff --git a/src/dusk/script/core/array/scriptarraysort.c b/src/dusk/script/core/array/scriptarraysort.c new file mode 100644 index 00000000..7598e600 --- /dev/null +++ b/src/dusk/script/core/array/scriptarraysort.c @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "array/scriptarraysort.h" +#include "value/scriptalloc.h" +#include "call/scriptcall.h" +#include "string/scriptstringchar.h" +#include "value/scriptvalueprimitive.h" +#include "number/scriptnumberunary.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" +#include + +int scriptCompareValues(scriptruntime_t *runtime, const scriptvalue_t a, const scriptvalue_t b, const scriptvalue_t comparer) { + scriptvalue_t args[2]; + scriptvalue_t result; + float number; + size_t i; + size_t shortest; + char x; + char y; + + if(comparer.type == SCRIPT_VALUE_FUNCTION) { + args[0] = a; + args[1] = b; + result = scriptCallFunction(runtime, comparer, args, 2); + if(!scriptIsNumber(result)) scriptRaise(runtime, "The comparison function has to return a number, got %s", scriptTypeName(result)); + number = result.as.number; + return number < 0 ? -1 : (number > 0 ? 1 : 0); + } + + if(scriptIsNumber(a) && scriptIsNumber(b)) return scriptNumberCompare(a, b); + + // Strings sort alphabetically, ignoring case, with case deciding ties + if(a.type == SCRIPT_VALUE_STRING && b.type == SCRIPT_VALUE_STRING) { + shortest = a.as.string->length < b.as.string->length ? a.as.string->length : b.as.string->length; + for(i = 0; i < shortest; i++) { + x = scriptCharLower(a.as.string->data[i]); + y = scriptCharLower(b.as.string->data[i]); + if(x != y) return (unsigned char)x < (unsigned char)y ? -1 : 1; + } + if(a.as.string->length != b.as.string->length) return a.as.string->length < b.as.string->length ? -1 : 1; + for(i = 0; i < shortest; i++) { + if(a.as.string->data[i] != b.as.string->data[i]) { + return (unsigned char)a.as.string->data[i] < (unsigned char)b.as.string->data[i] ? -1 : 1; + } + } + return 0; + } + + scriptRaise( + runtime, + "Only numbers and strings can be sorted without a comparison function, got %s and %s", + scriptTypeName(a), + scriptTypeName(b) + ); + return 0; +} + +void scriptSortValues( + scriptruntime_t *runtime, + scriptvalue_t *items, + const uint16_t count, + const bool descending, + const scriptvalue_t comparer +) { + scriptvalue_t *temp; + uint16_t width; + uint16_t start; + + if(count < 2) return; + temp = scriptAlloc(runtime, sizeof(scriptvalue_t) * count); + + for(width = 1; width < count; width *= 2) { + for(start = 0; start < count; start += 2 * width) { + uint16_t middle = start + width < count ? start + width : count; + uint16_t end = start + 2 * width < count ? start + 2 * width : count; + uint16_t left = start; + uint16_t right = middle; + uint16_t out = start; + + while(left < middle && right < end) { + int order = scriptCompareValues(runtime, items[left], items[right], comparer); + if(descending) order = -order; + if(order <= 0) temp[out++] = items[left++]; + else temp[out++] = items[right++]; + } + while(left < middle) temp[out++] = items[left++]; + while(right < end) temp[out++] = items[right++]; + } + memoryCopy(items, temp, sizeof(scriptvalue_t) * count); + } + + scriptFree(runtime, temp); +} diff --git a/src/dusk/script/core/array/scriptarraysort.h b/src/dusk/script/core/array/scriptarraysort.h new file mode 100644 index 00000000..4df056b1 --- /dev/null +++ b/src/dusk/script/core/array/scriptarraysort.h @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ARRAY_SCRIPTARRAYSORT_H +#define SCRIPT_ARRAY_SCRIPTARRAYSORT_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +/** + * Compares two values for sorting. + * + * @param runtime The runtime. + * @param a First value. + * @param b Second value. + * @param comparer A comparison function or undefined. + * @returns Negative, zero or positive. + */ +int scriptCompareValues(scriptruntime_t *runtime, const scriptvalue_t a, const scriptvalue_t b, const scriptvalue_t comparer); + +/** + * Sorts values in place, a stable merge sort. + * + * @param runtime The runtime. + * @param items The values. + * @param count Number of values. + * @param descending Whether to sort biggest first. + * @param comparer A function(a, b) returning a number, or undefined to compare + * numbers and strings directly. + */ +void scriptSortValues( + scriptruntime_t *runtime, + scriptvalue_t *items, + const uint16_t count, + const bool descending, + const scriptvalue_t comparer +); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarraybasic.c b/src/dusk/script/core/builtin/array/scriptbarraybasic.c new file mode 100644 index 00000000..046e0cf0 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraybasic.c @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarraybasic.h" +#include "builtin/scriptarg.h" +#include "array/scriptarraycopy.h" +#include "array/scriptarraymodify.h" +#include "value/scriptvalueprimitive.h" +#include "array/scriptarraydeep.h" +#include "error/scriptraise.h" +#include "string/scriptstringreject.h" +#include "value/scriptvaluecompare.h" + +SCRIPT_NATIVE(scriptBLen) { + scriptArgCount(runtime, argc, 1, 1); + if(args[0].type == SCRIPT_VALUE_ARRAY) return scriptNumber((float)args[0].as.array->length); + if(args[0].type == SCRIPT_VALUE_STRING) return scriptNumber((float)args[0].as.string->length); + scriptRaise(runtime, "len needs an array or a string, got %s", scriptTypeName(args[0])); + return scriptUndefined(); +} + +SCRIPT_NATIVE(scriptBPush) { + scriptarray_t *array; + scriptArgCount(runtime, argc, 2, 2); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + scriptArrayPush(runtime, array, args[1]); + return scriptNumber((float)array->length); +} + +SCRIPT_NATIVE(scriptBPop) { + scriptarray_t *array; + scriptArgCount(runtime, argc, 1, 1); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + if(array->length == 0) return scriptUndefined(); + return scriptArrayRemove(array, (uint16_t)(array->length - 1)); +} + +SCRIPT_NATIVE(scriptBShift) { + scriptarray_t *array; + scriptArgCount(runtime, argc, 1, 1); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + if(array->length == 0) return scriptUndefined(); + return scriptArrayRemove(array, 0); +} + +SCRIPT_NATIVE(scriptBUnshift) { + scriptarray_t *array; + scriptArgCount(runtime, argc, 2, 2); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + scriptArrayInsert(runtime, array, 0, args[1]); + return scriptNumber((float)array->length); +} + +SCRIPT_NATIVE(scriptBEmpty) { + scriptarray_t *array; + scriptArgCount(runtime, argc, 1, 1); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + array->length = 0; + array->category = SCRIPT_CATEGORY_UNKNOWN; + return args[0]; +} + +SCRIPT_NATIVE(scriptBClone) { + scriptArgCount(runtime, argc, 1, 1); + if(args[0].type == SCRIPT_VALUE_STRING) return args[0]; + return scriptArrayCopy(runtime, scriptArgArray(runtime, args, argc, 0)); +} + +SCRIPT_NATIVE(scriptBDeepClone) { + scriptArgCount(runtime, argc, 1, 1); + return scriptDeepCopy(runtime, args[0], 0); +} + +SCRIPT_NATIVE(scriptBDeepEqual) { + scriptArgCount(runtime, argc, 2, 2); + return scriptBool(scriptDeepEqual(args[0], args[1], 0)); +} diff --git a/src/dusk/script/core/builtin/array/scriptbarraybasic.h b/src/dusk/script/core/builtin/array/scriptbarraybasic.h new file mode 100644 index 00000000..5e5ab07e --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraybasic.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYBASIC_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYBASIC_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +// Arrays +SCRIPT_NATIVE(scriptBLen); + +SCRIPT_NATIVE(scriptBPush); + +SCRIPT_NATIVE(scriptBPop); + +SCRIPT_NATIVE(scriptBShift); + +SCRIPT_NATIVE(scriptBUnshift); + +SCRIPT_NATIVE(scriptBEmpty); + +SCRIPT_NATIVE(scriptBClone); + +SCRIPT_NATIVE(scriptBDeepClone); + +SCRIPT_NATIVE(scriptBDeepEqual); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarrayfunc.c b/src/dusk/script/core/builtin/array/scriptbarrayfunc.c new file mode 100644 index 00000000..ca823bc3 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayfunc.c @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarrayfunc.h" +#include "builtin/scriptarg.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "call/scriptcall.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBMap) { + scriptarray_t *array; + scriptvalue_t result = scriptArrayNew(runtime); + scriptvalue_t callArgs[3]; + uint16_t i; + + scriptArgCount(runtime, argc, 2, 2); + array = scriptArgArray(runtime, args, argc, 0); + scriptArgFunction(runtime, args, argc, 1); + + for(i = 0; i < array->length; i++) { + callArgs[0] = array->items[i]; + callArgs[1] = scriptNumber((float)i); + callArgs[2] = args[0]; + scriptArrayPush(runtime, result.as.array, scriptCallFunction(runtime, args[1], callArgs, 3)); + } + return result; +} + +SCRIPT_NATIVE(scriptBReduce) { + scriptarray_t *array; + scriptvalue_t accumulator; + scriptvalue_t callArgs[4]; + uint16_t i; + + scriptArgCount(runtime, argc, 3, 3); + array = scriptArgArray(runtime, args, argc, 0); + scriptArgFunction(runtime, args, argc, 1); + accumulator = args[2]; + + for(i = 0; i < array->length; i++) { + callArgs[0] = accumulator; + callArgs[1] = array->items[i]; + callArgs[2] = scriptNumber((float)i); + callArgs[3] = args[0]; + accumulator = scriptCallFunction(runtime, args[1], callArgs, 4); + } + return accumulator; +} diff --git a/src/dusk/script/core/builtin/array/scriptbarrayfunc.h b/src/dusk/script/core/builtin/array/scriptbarrayfunc.h new file mode 100644 index 00000000..aa65357c --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayfunc.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYFUNC_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYFUNC_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBMap); + +SCRIPT_NATIVE(scriptBReduce); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarraymodify.c b/src/dusk/script/core/builtin/array/scriptbarraymodify.c new file mode 100644 index 00000000..5f1de894 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraymodify.c @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarraymodify.h" +#include "builtin/scriptarg.h" +#include "array/scriptarraymodify.h" +#include "array/scriptarraynew.h" +#include "value/scriptvalueprimitive.h" +#include "error/scriptraise.h" +#include "string/scriptstringreject.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBFill) { + scriptarray_t *array; + int32_t count; + int32_t i; + + scriptArgCount(runtime, argc, 3, 3); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + count = scriptArgInteger(runtime, args, argc, 2); + if(count < 0 || count > SCRIPT_ARRAY_LENGTH_MAX) scriptRaise(runtime, "fill needs a count from 0 to %d", SCRIPT_ARRAY_LENGTH_MAX); + + array->length = 0; + array->category = SCRIPT_CATEGORY_UNKNOWN; + for(i = 0; i < count; i++) scriptArrayPush(runtime, array, args[1]); + return args[0]; +} + +SCRIPT_NATIVE(scriptBMerge) { + scriptarray_t *array; + scriptarray_t *other; + uint16_t count; + uint16_t i; + + scriptArgCount(runtime, argc, 2, 2); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + other = scriptArgArray(runtime, args, argc, 1); + count = other->length; + for(i = 0; i < count; i++) scriptArrayPush(runtime, array, other->items[i]); + return scriptNumber((float)array->length); +} + +SCRIPT_NATIVE(scriptBSplice) { + scriptarray_t *array; + scriptvalue_t removed; + int32_t start; + int32_t count = 1; + int32_t i; + + scriptArgCount(runtime, argc, 2, 3); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + start = scriptArgInteger(runtime, args, argc, 1); + if(argc > 2) count = scriptArgInteger(runtime, args, argc, 2); + if(start < 0 || start > array->length) scriptRaise(runtime, "Index %d is out of bounds, the array holds %u elements", (int)start, (unsigned int)array->length); + if(count < 0) scriptRaise(runtime, "splice can not remove a negative number of elements"); + if(count > array->length - start) count = array->length - start; + + removed = scriptArrayNew(runtime); + for(i = 0; i < count; i++) scriptArrayPushRaw(runtime, removed.as.array, array->items[start + i]); + removed.as.array->category = array->category; + for(i = 0; i < count; i++) scriptArrayRemove(array, (uint16_t)start); + return removed; +} + +SCRIPT_NATIVE(scriptBInsert) { + scriptarray_t *array; + int32_t index; + + scriptArgCount(runtime, argc, 3, 3); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + index = scriptArgInteger(runtime, args, argc, 1); + if(index < 0 || index > array->length) scriptRaise(runtime, "Index %d is out of bounds, the array holds %u elements", (int)index, (unsigned int)array->length); + scriptArrayInsert(runtime, array, (uint16_t)index, args[2]); + return scriptNumber((float)array->length); +} + +SCRIPT_NATIVE(scriptBInsertArr) { + scriptarray_t *array; + scriptarray_t *other; + int32_t index; + uint16_t count; + uint16_t i; + + scriptArgCount(runtime, argc, 3, 3); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + index = scriptArgInteger(runtime, args, argc, 1); + other = scriptArgArray(runtime, args, argc, 2); + if(index < 0 || index > array->length) scriptRaise(runtime, "Index %d is out of bounds, the array holds %u elements", (int)index, (unsigned int)array->length); + count = other->length; + for(i = 0; i < count; i++) scriptArrayInsert(runtime, array, (uint16_t)(index + i), other->items[i]); + return scriptNumber((float)array->length); +} diff --git a/src/dusk/script/core/builtin/array/scriptbarraymodify.h b/src/dusk/script/core/builtin/array/scriptbarraymodify.h new file mode 100644 index 00000000..1cda8b19 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraymodify.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYMODIFY_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYMODIFY_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBFill); + +SCRIPT_NATIVE(scriptBMerge); + +SCRIPT_NATIVE(scriptBSplice); + +SCRIPT_NATIVE(scriptBInsert); + +SCRIPT_NATIVE(scriptBInsertArr); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarrayorder.c b/src/dusk/script/core/builtin/array/scriptbarrayorder.c new file mode 100644 index 00000000..d5bf7662 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayorder.c @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarrayorder.h" +#include "builtin/scriptarg.h" +#include "builtin/math/scriptbmathrandom.h" +#include "string/scriptstringreject.h" +#include "array/scriptarraysort.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBSort) { + scriptarray_t *array; + scriptvalue_t comparer = scriptUndefined(); + + scriptArgCount(runtime, argc, 1, 3); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + if(argc > 2) comparer = scriptArgFunction(runtime, args, argc, 2); + + scriptSortValues(runtime, array->items, array->length, argc > 1 && scriptTruthy(args[1]), comparer); + return args[0]; +} + +SCRIPT_NATIVE(scriptBReverse) { + scriptarray_t *array; + uint16_t i; + scriptArgCount(runtime, argc, 1, 1); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + for(i = 0; i < array->length / 2; i++) { + scriptvalue_t swap = array->items[i]; + array->items[i] = array->items[array->length - 1 - i]; + array->items[array->length - 1 - i] = swap; + } + return args[0]; +} + +SCRIPT_NATIVE(scriptBShuffle) { + scriptarray_t *array; + uint16_t i; + scriptArgCount(runtime, argc, 1, 1); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + for(i = array->length; i > 1; i--) { + uint16_t pick = (uint16_t)(scriptRandomNext(runtime) % i); + scriptvalue_t swap = array->items[i - 1]; + array->items[i - 1] = array->items[pick]; + array->items[pick] = swap; + } + return args[0]; +} diff --git a/src/dusk/script/core/builtin/array/scriptbarrayorder.h b/src/dusk/script/core/builtin/array/scriptbarrayorder.h new file mode 100644 index 00000000..5aa9f4d5 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayorder.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYORDER_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYORDER_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBSort); + +SCRIPT_NATIVE(scriptBReverse); + +SCRIPT_NATIVE(scriptBShuffle); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarraysearch.c b/src/dusk/script/core/builtin/array/scriptbarraysearch.c new file mode 100644 index 00000000..4c68a300 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraysearch.c @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarraysearch.h" +#include "value/scriptalloc.h" +#include "builtin/scriptarg.h" +#include "call/scriptcall.h" +#include "value/scriptvalueprimitive.h" +#include "error/scriptraise.h" +#include "string/scriptstringreject.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBIndexOf) { + uint16_t i; + scriptArgCount(runtime, argc, 2, 2); + + if(args[0].type == SCRIPT_VALUE_STRING) { + scriptstring_t *needle = scriptArgString(runtime, args, argc, 1); + if(needle->length != 1) scriptRaise(runtime, "On a string indexof finds a single char, use strindex for a piece of text"); + for(i = 0; i < args[0].as.string->length; i++) { + if(args[0].as.string->data[i] == needle->data[0]) return scriptNumber((float)i); + } + return scriptNumber(-1); + } + + { + scriptarray_t *array = scriptArgArray(runtime, args, argc, 0); + for(i = 0; i < array->length; i++) { + if(scriptValuesEqual(array->items[i], args[1])) return scriptNumber((float)i); + } + } + return scriptNumber(-1); +} + +SCRIPT_NATIVE(scriptBLastIndexOf) { + int32_t i; + scriptArgCount(runtime, argc, 2, 2); + + if(args[0].type == SCRIPT_VALUE_STRING) { + scriptstring_t *needle = scriptArgString(runtime, args, argc, 1); + if(needle->length != 1) scriptRaise(runtime, "On a string lastindexof finds a single char"); + for(i = (int32_t)args[0].as.string->length - 1; i >= 0; i--) { + if(args[0].as.string->data[i] == needle->data[0]) return scriptNumber((float)i); + } + return scriptNumber(-1); + } + + { + scriptarray_t *array = scriptArgArray(runtime, args, argc, 0); + for(i = (int32_t)array->length - 1; i >= 0; i--) { + if(scriptValuesEqual(array->items[i], args[1])) return scriptNumber((float)i); + } + } + return scriptNumber(-1); +} + +SCRIPT_NATIVE(scriptBFilter) { + scriptarray_t *array; + bool *keep; + uint16_t length; + uint16_t i; + uint16_t kept = 0; + scriptvalue_t callArgs[3]; + + scriptArgCount(runtime, argc, 1, 2); + scriptRejectString(runtime, args[0]); + array = scriptArgArrayWritable(runtime, args, argc, 0); + if(argc > 1) scriptArgFunction(runtime, args, argc, 1); + + length = array->length; + keep = scriptAlloc(runtime, sizeof(bool) * (length ? length : 1)); + for(i = 0; i < length; i++) { + if(argc > 1) { + callArgs[0] = array->items[i]; + callArgs[1] = scriptNumber((float)i); + callArgs[2] = args[0]; + keep[i] = scriptTruthy(scriptCallFunction(runtime, args[1], callArgs, 3)); + } else { + keep[i] = array->items[i].type != SCRIPT_VALUE_NULL; + } + } + + for(i = 0; i < length; i++) { + if(keep[i]) array->items[kept++] = array->items[i]; + } + array->length = kept; + scriptFree(runtime, keep); + return args[0]; +} diff --git a/src/dusk/script/core/builtin/array/scriptbarraysearch.h b/src/dusk/script/core/builtin/array/scriptbarraysearch.h new file mode 100644 index 00000000..a2b4f587 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarraysearch.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYSEARCH_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYSEARCH_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBIndexOf); + +SCRIPT_NATIVE(scriptBLastIndexOf); + +SCRIPT_NATIVE(scriptBFilter); + +#endif diff --git a/src/dusk/script/core/builtin/array/scriptbarrayslice.c b/src/dusk/script/core/builtin/array/scriptbarrayslice.c new file mode 100644 index 00000000..1f73c988 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayslice.c @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/array/scriptbarrayslice.h" +#include "builtin/scriptarg.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "string/scriptbuf.h" +#include "error/scriptraise.h" +#include "string/scriptstringnew.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptvalueformat.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBSubarr) { + int32_t start; + int32_t end; + int32_t length; + uint16_t i; + scriptvalue_t result; + + scriptArgCount(runtime, argc, 2, 3); + if(args[0].type == SCRIPT_VALUE_STRING) { + length = args[0].as.string->length; + } else { + length = scriptArgArray(runtime, args, argc, 0)->length; + } + + start = scriptArgInteger(runtime, args, argc, 1); + end = argc > 2 ? scriptArgInteger(runtime, args, argc, 2) : length; + if(start < 0 || start > length || end < start || end > length) { + scriptRaise(runtime, "The range %d to %d is out of bounds, there are %d elements", (int)start, (int)end, (int)length); + } + + if(args[0].type == SCRIPT_VALUE_STRING) { + // The offset is 16 bits, the compiler for the SNES adds a 32 bit one to a pointer wrongly + const char *text = args[0].as.string->data; + uint16_t from = (uint16_t)start; + uint16_t count = (uint16_t)(end - start); + return scriptStringNew(runtime, text + from, count); + } + + result = scriptArrayNew(runtime); + for(i = (uint16_t)start; i < end; i++) scriptArrayPushRaw(runtime, result.as.array, args[0].as.array->items[i]); + result.as.array->category = args[0].as.array->category; + return result; +} + +SCRIPT_NATIVE(scriptBFlatten) { + scriptarray_t *array; + scriptvalue_t result = scriptArrayNew(runtime); + scriptcategory_t category = SCRIPT_CATEGORY_UNKNOWN; + uint16_t i; + uint16_t j; + + scriptArgCount(runtime, argc, 1, 1); + array = scriptArgArray(runtime, args, argc, 0); + + for(i = 0; i < array->length; i++) { + scriptarray_t *inner; + if(array->items[i].type != SCRIPT_VALUE_ARRAY) { + scriptRaise(runtime, "flatten needs an array of arrays, found %s", scriptTypeName(array->items[i])); + } + inner = array->items[i].as.array; + if(inner->category != SCRIPT_CATEGORY_UNKNOWN) { + if(category == SCRIPT_CATEGORY_UNKNOWN) category = inner->category; + else if(category != inner->category) scriptRaise(runtime, "flatten only works on arrays that all hold the same type"); + } + for(j = 0; j < inner->length; j++) scriptArrayPushRaw(runtime, result.as.array, inner->items[j]); + } + + result.as.array->category = category; + return result; +} + +SCRIPT_NATIVE(scriptBJoin) { + scriptarray_t *array; + scriptstring_t *separator; + scriptbuf_t buffer; + scriptvalue_t result; + uint16_t i; + + scriptArgCount(runtime, argc, 2, 2); + array = scriptArgArray(runtime, args, argc, 0); + separator = scriptArgString(runtime, args, argc, 1); + + scriptBufInit(&buffer); + for(i = 0; i < array->length; i++) { + if(i) scriptBufAppend(runtime, &buffer, separator->data, separator->length); + scriptValueFormat(runtime, &buffer, array->items[i], false); + } + result = scriptStringNew(runtime, buffer.data ? buffer.data : "", buffer.length); + scriptBufFree(runtime, &buffer); + return result; +} diff --git a/src/dusk/script/core/builtin/array/scriptbarrayslice.h b/src/dusk/script/core/builtin/array/scriptbarrayslice.h new file mode 100644 index 00000000..06a3a914 --- /dev/null +++ b/src/dusk/script/core/builtin/array/scriptbarrayslice.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYSLICE_H +#define SCRIPT_BUILTIN_ARRAY_SCRIPTBARRAYSLICE_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBSubarr); + +SCRIPT_NATIVE(scriptBFlatten); + +SCRIPT_NATIVE(scriptBJoin); + +#endif diff --git a/src/dusk/script/core/builtin/core/scriptbconvert.c b/src/dusk/script/core/builtin/core/scriptbconvert.c new file mode 100644 index 00000000..fe358245 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbconvert.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "builtin/core/scriptbconvert.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "string/scriptbuf.h" +#include "number/scriptnumberformat.h" +#include "number/scriptnumberparse.h" +#include "error/scriptraise.h" +#include "string/scriptstringnew.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptvalueformat.h" +#include "value/scriptfunction.h" +#include +#include + +SCRIPT_NATIVE(scriptBToStr) { + scriptbuf_t buffer; + scriptvalue_t result; + int decimals = -1; + + scriptArgCount(runtime, argc, 1, 2); + if(argc > 1) { + if(!scriptIsNumber(args[0])) scriptRaise(runtime, "Only numbers can have decimal places, got %s", scriptTypeName(args[0])); + decimals = (int)scriptArgInteger(runtime, args, argc, 1); + if(decimals < 0) scriptRaise(runtime, "Decimal places can not be negative"); + } + + if(args[0].type == SCRIPT_VALUE_STRING) return args[0]; + + scriptBufInit(&buffer); + if(scriptIsNumber(args[0])) scriptNumberFormat(runtime, &buffer, args[0], decimals); + else scriptValueFormat(runtime, &buffer, args[0], false); + result = scriptStringNew(runtime, buffer.data ? buffer.data : "", buffer.length); + scriptBufFree(runtime, &buffer); + return result; +} + +SCRIPT_NATIVE(scriptBToBase) { + scriptvalue_t value; + int base; + int32_t number; + bool negative; + char digits[40]; + uint8_t count = 0; + char text[41]; + uint8_t i; + + scriptArgCount(runtime, argc, 2, 2); + value = args[0]; + base = (int)scriptArgInteger(runtime, args, argc, 1); + if(base < 2 || base > 36) scriptRaise(runtime, "The base has to be from 2 to 36"); + + if(value.type == SCRIPT_VALUE_BOOL) { + number = value.as.boolean ? 1 : 0; + } else if(value.type == SCRIPT_VALUE_NUMBER && value.as.number == floorf(value.as.number)) { + number = (int32_t)value.as.number; + } else { + scriptRaise(runtime, "tobase needs a whole number or a bool, got %s", scriptTypeName(value)); + return scriptUndefined(); + } + + negative = number < 0; + if(negative) number = -number; + do { + int digit = (int)(number % base); + digits[count++] = (char)(digit < 10 ? '0' + digit : 'A' + digit - 10); + number /= base; + } while(number > 0); + + i = 0; + if(negative) text[i++] = '-'; + while(count > 0) text[i++] = digits[--count]; + return scriptStringNew(runtime, text, i); +} + +SCRIPT_NATIVE(scriptBToNum) { + scriptvalue_t value = scriptArgOptional(args, argc, 0); + scriptstring_t *string; + scriptparse_t parsed; + scriptvalue_t result; + int32_t integer; + + scriptArgCount(runtime, argc, 1, 2); + if(scriptIsNumber(value)) return value; + string = scriptArgString(runtime, args, argc, 0); + + if(argc > 1) { + parsed = scriptParseInteger(string->data, string->length, (int)scriptArgInteger(runtime, args, argc, 1), &integer); + result = scriptNumber((float)integer); + } else { + parsed = scriptParseDecimal(string->data, string->length, &result); + } + + if(parsed == SCRIPT_PARSE_INVALID) scriptRaise(runtime, "Can not turn \"%s\" into a number", string->data); + if(parsed == SCRIPT_PARSE_TOOBIG) scriptRaise(runtime, "\"%s\" is too big a number", string->data); + return result; +} + +SCRIPT_NATIVE(scriptBToBool) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = args[0]; + + if(value.type == SCRIPT_VALUE_BOOL) return value; + if(scriptIsNumber(value)) return scriptBool(scriptTruthy(value)); + if(value.type == SCRIPT_VALUE_STRING) { + if(value.as.string->length == 4 && memoryCompare(value.as.string->data, "true", 4) == 0) return scriptBool(true); + if(value.as.string->length == 5 && memoryCompare(value.as.string->data, "false", 5) == 0) return scriptBool(false); + } + scriptRaise(runtime, "Can not turn this %s into a bool", scriptTypeName(value)); + return scriptUndefined(); +} diff --git a/src/dusk/script/core/builtin/core/scriptbconvert.h b/src/dusk/script/core/builtin/core/scriptbconvert.h new file mode 100644 index 00000000..0b3a9ee0 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbconvert.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_CORE_SCRIPTBCONVERT_H +#define SCRIPT_BUILTIN_CORE_SCRIPTBCONVERT_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBToStr); + +SCRIPT_NATIVE(scriptBToBase); + +SCRIPT_NATIVE(scriptBToNum); + +SCRIPT_NATIVE(scriptBToBool); + +#endif diff --git a/src/dusk/script/core/builtin/core/scriptbinclude.c b/src/dusk/script/core/builtin/core/scriptbinclude.c new file mode 100644 index 00000000..bb94d9e9 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbinclude.c @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/core/scriptbinclude.h" +#include "builtin/scriptarg.h" +#include "call/scriptcall.h" +#include "runtime/scriptruntimeload.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptfunction.h" + +// A script that is compiled with the assets of the game has the path of what it +// includes turned into the number of the asset, and an include with a number looks +// the script up by it. Anything else is a path, read when it is included. +static scriptvalue_t scriptInclude(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const bool rootAsync) { + scriptvalue_t function; + + scriptArgCount(runtime, argc, 1, 1); + if(args[0].type == SCRIPT_VALUE_NUMBER) { + function = scriptLoadAsset(runtime, (uint16_t)args[0].as.number, rootAsync); + } else { + function = scriptLoadScript(runtime, scriptArgString(runtime, args, argc, 0)->data, rootAsync); + } + return scriptCallFunction(runtime, function, NULL, 0); +} + +SCRIPT_NATIVE(scriptBInclude) { + return scriptInclude(runtime, args, argc, false); +} + +SCRIPT_NATIVE(scriptBAInclude) { + return scriptInclude(runtime, args, argc, true); +} diff --git a/src/dusk/script/core/builtin/core/scriptbinclude.h b/src/dusk/script/core/builtin/core/scriptbinclude.h new file mode 100644 index 00000000..abed76ca --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbinclude.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_CORE_SCRIPTBINCLUDE_H +#define SCRIPT_BUILTIN_CORE_SCRIPTBINCLUDE_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBInclude); + +SCRIPT_NATIVE(scriptBAInclude); + +#endif diff --git a/src/dusk/script/core/builtin/core/scriptbprint.c b/src/dusk/script/core/builtin/core/scriptbprint.c new file mode 100644 index 00000000..87307ae5 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbprint.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/core/scriptbprint.h" +#include "builtin/scriptarg.h" +#include "string/scriptbuf.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptvalueformat.h" + +#if SCRIPT_FEATURE_PRINT +SCRIPT_NATIVE(scriptBPrint) { + scriptbuf_t buffer; + uint8_t i; + + scriptBufInit(&buffer); + for(i = 0; i < argc; i++) { + if(i) scriptBufAppend(runtime, &buffer, " ", 1); + scriptValueFormat(runtime, &buffer, args[i], false); + } + scriptBufAppend(runtime, &buffer, "\n", 1); + if(runtime->printHandler) runtime->printHandler(buffer.data, buffer.length); + scriptBufFree(runtime, &buffer); + return scriptUndefined(); +} +#endif + +SCRIPT_NATIVE(scriptBAssert) { + scriptArgCount(runtime, argc, 1, 1); + if(!scriptTruthy(args[0])) scriptRaise(runtime, "Assertion failed"); + return scriptUndefined(); +} diff --git a/src/dusk/script/core/builtin/core/scriptbprint.h b/src/dusk/script/core/builtin/core/scriptbprint.h new file mode 100644 index 00000000..b5aa876a --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbprint.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_CORE_SCRIPTBPRINT_H +#define SCRIPT_BUILTIN_CORE_SCRIPTBPRINT_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +// Core +SCRIPT_NATIVE(scriptBPrint); + +SCRIPT_NATIVE(scriptBAssert); + +#endif diff --git a/src/dusk/script/core/builtin/core/scriptbpromise.c b/src/dusk/script/core/builtin/core/scriptbpromise.c new file mode 100644 index 00000000..c614343d --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbpromise.c @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/core/scriptbpromise.h" +#include "builtin/scriptarg.h" +#include "call/scriptcall.h" +#include "value/scriptfunction.h" +#include "promise/scriptpromisecombine.h" +#include "promise/scriptpromisenew.h" +#include "promise/scriptpromisesettle.h" +#include "error/scriptraise.h" +#include "promise/scripttimer.h" +#include "value/scriptvalueprimitive.h" + +SCRIPT_NATIVE(scriptBResolve) { + scriptPromiseResolve(runtime, (scriptpromise_t *)self->data, scriptArgOptional(args, argc, 0)); + return scriptUndefined(); +} + +SCRIPT_NATIVE(scriptBReject) { + scriptPromiseReject(runtime, (scriptpromise_t *)self->data, scriptArgOptional(args, argc, 0)); + return scriptUndefined(); +} + +SCRIPT_NATIVE(scriptBPromise) { + scriptvalue_t promise; + scriptvalue_t executor; + scriptvalue_t callbacks[2]; + + scriptArgCount(runtime, argc, 1, 1); + executor = scriptArgFunction(runtime, args, argc, 0); + promise = scriptPromiseNew(runtime); + callbacks[0] = scriptFunctionNative(runtime, "resolve", scriptBResolve, promise.as.promise); + callbacks[1] = scriptFunctionNative(runtime, "reject", scriptBReject, promise.as.promise); + + scriptCallFunction(runtime, executor, callbacks, 2); + return promise; +} + +SCRIPT_NATIVE(scriptBAll) { + scriptArgCount(runtime, argc, 1, 1); + return scriptPromiseCombine(runtime, SCRIPT_COMBO_ALL, scriptArgArray(runtime, args, argc, 0)); +} + +SCRIPT_NATIVE(scriptBAny) { + scriptArgCount(runtime, argc, 1, 1); + return scriptPromiseCombine(runtime, SCRIPT_COMBO_ANY, scriptArgArray(runtime, args, argc, 0)); +} + +SCRIPT_NATIVE(scriptBFirst) { + scriptArgCount(runtime, argc, 1, 1); + return scriptPromiseCombine(runtime, SCRIPT_COMBO_FIRST, scriptArgArray(runtime, args, argc, 0)); +} + +SCRIPT_NATIVE(scriptBWait) { + int32_t ticks; + scriptArgCount(runtime, argc, 1, 1); + ticks = scriptArgInteger(runtime, args, argc, 0); + if(ticks < 0) scriptRaise(runtime, "Can not wait a negative number of ticks"); + return scriptTimerPromise(runtime, (uint32_t)ticks); +} diff --git a/src/dusk/script/core/builtin/core/scriptbpromise.h b/src/dusk/script/core/builtin/core/scriptbpromise.h new file mode 100644 index 00000000..55625259 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbpromise.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_CORE_SCRIPTBPROMISE_H +#define SCRIPT_BUILTIN_CORE_SCRIPTBPROMISE_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBResolve); + +SCRIPT_NATIVE(scriptBReject); + +SCRIPT_NATIVE(scriptBPromise); + +SCRIPT_NATIVE(scriptBAll); + +SCRIPT_NATIVE(scriptBAny); + +SCRIPT_NATIVE(scriptBFirst); + +SCRIPT_NATIVE(scriptBWait); + +#endif diff --git a/src/dusk/script/core/builtin/core/scriptbtype.c b/src/dusk/script/core/builtin/core/scriptbtype.c new file mode 100644 index 00000000..f784597b --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbtype.c @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/core/scriptbtype.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" + +SCRIPT_NATIVE(scriptBIsNull) { + return scriptBool(scriptArgType(args, argc, 0) == SCRIPT_VALUE_NULL); +} + +SCRIPT_NATIVE(scriptBIsUndefined) { + return scriptBool(scriptArgType(args, argc, 0) == SCRIPT_VALUE_UNDEFINED); +} + +SCRIPT_NATIVE(scriptBIsNullOrUndefined) { + scriptvaluetype_t type = scriptArgType(args, argc, 0); + return scriptBool(type == SCRIPT_VALUE_UNDEFINED || type == SCRIPT_VALUE_NULL); +} + +SCRIPT_NATIVE(scriptBIsFunctionAsync) { + if(scriptArgType(args, argc, 0) != SCRIPT_VALUE_FUNCTION) return scriptBool(false); + return scriptBool(args[0].as.function->isAsync); +} + +SCRIPT_NATIVE(scriptBIsFunction) { + return scriptBool(scriptArgType(args, argc, 0) == SCRIPT_VALUE_FUNCTION); +} diff --git a/src/dusk/script/core/builtin/core/scriptbtype.h b/src/dusk/script/core/builtin/core/scriptbtype.h new file mode 100644 index 00000000..a8caedc6 --- /dev/null +++ b/src/dusk/script/core/builtin/core/scriptbtype.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_CORE_SCRIPTBTYPE_H +#define SCRIPT_BUILTIN_CORE_SCRIPTBTYPE_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBIsNull); + +SCRIPT_NATIVE(scriptBIsUndefined); + +SCRIPT_NATIVE(scriptBIsNullOrUndefined); + +SCRIPT_NATIVE(scriptBIsFunction); + +SCRIPT_NATIVE(scriptBIsFunctionAsync); + +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathbasic.c b/src/dusk/script/core/builtin/math/scriptbmathbasic.c new file mode 100644 index 00000000..a80380d1 --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathbasic.c @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/math/scriptbmathbasic.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "number/scriptnumberbinary.h" +#include "number/scriptnumberunary.h" +#include "error/scriptraise.h" +#include "value/scriptfunction.h" +#include + +SCRIPT_NATIVE(scriptBAbs) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = scriptArgNumber(runtime, args, argc, 0); + return scriptNumber(fabsf(value.as.number)); +} + +SCRIPT_NATIVE(scriptBSign) { + float number; + scriptArgCount(runtime, argc, 1, 1); + number = scriptArgNumber(runtime, args, argc, 0).as.number; + return scriptNumber(number < 0 ? -1.0f : (number > 0 ? 1.0f : 0.0f)); +} + +SCRIPT_NATIVE(scriptBMin) { + scriptvalue_t a; + scriptvalue_t b; + scriptArgCount(runtime, argc, 2, 2); + a = scriptArgNumber(runtime, args, argc, 0); + b = scriptArgNumber(runtime, args, argc, 1); + return scriptNumberCompare(a, b) <= 0 ? a : b; +} + +SCRIPT_NATIVE(scriptBMax) { + scriptvalue_t a; + scriptvalue_t b; + scriptArgCount(runtime, argc, 2, 2); + a = scriptArgNumber(runtime, args, argc, 0); + b = scriptArgNumber(runtime, args, argc, 1); + return scriptNumberCompare(a, b) >= 0 ? a : b; +} + +SCRIPT_NATIVE(scriptBClamp) { + scriptvalue_t value; + scriptvalue_t low; + scriptvalue_t high; + scriptArgCount(runtime, argc, 3, 3); + value = scriptArgNumber(runtime, args, argc, 0); + low = scriptArgNumber(runtime, args, argc, 1); + high = scriptArgNumber(runtime, args, argc, 2); + if(scriptNumberCompare(value, low) < 0) return low; + if(scriptNumberCompare(value, high) > 0) return high; + return value; +} + +SCRIPT_NATIVE(scriptBPow) { + scriptvalue_t base; + scriptvalue_t result; + int32_t exponent; + int32_t i; + + scriptArgCount(runtime, argc, 2, 2); + base = scriptArgNumber(runtime, args, argc, 0); + exponent = scriptArgInteger(runtime, args, argc, 1); + + if(exponent < 0) { +#if SCRIPT_FEATURE_MATH + return scriptNumber(powf(base.as.number, (float)exponent)); +#else + scriptRaise(runtime, "Negative powers need a maths library, this platform has none"); +#endif + } + + result = scriptNumber(1); + for(i = 0; i < exponent; i++) result = scriptNumberBinary(runtime, SCRIPT_OP_MUL, result, base); + return result; +} + +#if SCRIPT_FEATURE_MATH +SCRIPT_NATIVE(scriptBSqrt) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = scriptArgNumber(runtime, args, argc, 0); + if(value.as.number < 0) scriptRaise(runtime, "Can not take the square root of a negative number"); + return scriptNumber(sqrtf(value.as.number)); +} +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathbasic.h b/src/dusk/script/core/builtin/math/scriptbmathbasic.h new file mode 100644 index 00000000..25fa48ae --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathbasic.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_MATH_SCRIPTBMATHBASIC_H +#define SCRIPT_BUILTIN_MATH_SCRIPTBMATHBASIC_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +// Maths +SCRIPT_NATIVE(scriptBAbs); + +SCRIPT_NATIVE(scriptBSign); + +SCRIPT_NATIVE(scriptBMin); + +SCRIPT_NATIVE(scriptBMax); + +SCRIPT_NATIVE(scriptBClamp); + +SCRIPT_NATIVE(scriptBPow); + +SCRIPT_NATIVE(scriptBSqrt); + +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathrandom.c b/src/dusk/script/core/builtin/math/scriptbmathrandom.c new file mode 100644 index 00000000..529fcfc4 --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathrandom.c @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/math/scriptbmathrandom.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "error/scriptraise.h" +#include "value/scriptfunction.h" + +uint32_t scriptRandomNext(scriptruntime_t *runtime) { + uint32_t x = runtime->randState; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + runtime->randState = x; + return x; +} + +SCRIPT_NATIVE(scriptBRand) { + int32_t low; + int32_t high; + int32_t result; + + scriptArgCount(runtime, argc, 2, 2); + low = scriptArgInteger(runtime, args, argc, 0); + high = scriptArgInteger(runtime, args, argc, 1); + if(high <= low) scriptRaise(runtime, "rand needs a maximum bigger than the minimum"); + + result = low + (int32_t)(scriptRandomNext(runtime) % (uint32_t)(high - low)); + return scriptNumber((float)result); +} + +SCRIPT_NATIVE(scriptBRandf) { + float low; + float high; + + scriptArgCount(runtime, argc, 2, 2); + low = scriptArgNumber(runtime, args, argc, 0).as.number; + high = scriptArgNumber(runtime, args, argc, 1).as.number; + if(high <= low) scriptRaise(runtime, "randf needs a maximum bigger than the minimum"); + + return scriptNumber(low + ((float)scriptRandomNext(runtime) / 4294967295.0f) * (high - low)); +} + +SCRIPT_NATIVE(scriptBRandSeed) { + scriptArgCount(runtime, argc, 1, 1); + runtime->randState = (uint32_t)scriptArgInteger(runtime, args, argc, 0); + if(runtime->randState == 0) runtime->randState = 1; + return scriptUndefined(); +} diff --git a/src/dusk/script/core/builtin/math/scriptbmathrandom.h b/src/dusk/script/core/builtin/math/scriptbmathrandom.h new file mode 100644 index 00000000..5dcd1b6c --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathrandom.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_MATH_SCRIPTBMATHRANDOM_H +#define SCRIPT_BUILTIN_MATH_SCRIPTBMATHRANDOM_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +/** + * Next number from the random generator. + * + * @param runtime The runtime. + * @returns A 32 bit random number. + */ +uint32_t scriptRandomNext(scriptruntime_t *runtime); + +SCRIPT_NATIVE(scriptBRand); + +SCRIPT_NATIVE(scriptBRandf); + +SCRIPT_NATIVE(scriptBRandSeed); + +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathround.c b/src/dusk/script/core/builtin/math/scriptbmathround.c new file mode 100644 index 00000000..769063ae --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathround.c @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/math/scriptbmathround.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptfunction.h" +#include + +SCRIPT_NATIVE(scriptBFloor) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = scriptArgNumber(runtime, args, argc, 0); + return scriptNumber(floorf(value.as.number)); +} + +SCRIPT_NATIVE(scriptBCeil) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = scriptArgNumber(runtime, args, argc, 0); + return scriptNumber(ceilf(value.as.number)); +} + +SCRIPT_NATIVE(scriptBRound) { + scriptvalue_t value; + scriptArgCount(runtime, argc, 1, 1); + value = scriptArgNumber(runtime, args, argc, 0); + return scriptNumber(roundf(value.as.number)); +} diff --git a/src/dusk/script/core/builtin/math/scriptbmathround.h b/src/dusk/script/core/builtin/math/scriptbmathround.h new file mode 100644 index 00000000..1954d01d --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathround.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_MATH_SCRIPTBMATHROUND_H +#define SCRIPT_BUILTIN_MATH_SCRIPTBMATHROUND_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBFloor); + +SCRIPT_NATIVE(scriptBCeil); + +SCRIPT_NATIVE(scriptBRound); + +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathtrig.c b/src/dusk/script/core/builtin/math/scriptbmathtrig.c new file mode 100644 index 00000000..a28c5086 --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathtrig.c @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/math/scriptbmathtrig.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#if SCRIPT_FEATURE_MATH +#include +#endif + +#if SCRIPT_FEATURE_MATH +SCRIPT_NATIVE(scriptBSin) { + scriptArgCount(runtime, argc, 1, 1); + return scriptNumber(sinf(scriptArgNumber(runtime, args, argc, 0).as.number)); +} + +SCRIPT_NATIVE(scriptBCos) { + scriptArgCount(runtime, argc, 1, 1); + return scriptNumber(cosf(scriptArgNumber(runtime, args, argc, 0).as.number)); +} + +SCRIPT_NATIVE(scriptBTan) { + scriptArgCount(runtime, argc, 1, 1); + return scriptNumber(tanf(scriptArgNumber(runtime, args, argc, 0).as.number)); +} + +SCRIPT_NATIVE(scriptBAtan2) { + scriptArgCount(runtime, argc, 2, 2); + return scriptNumber(atan2f( + scriptArgNumber(runtime, args, argc, 0).as.number, + scriptArgNumber(runtime, args, argc, 1).as.number + )); +} +#endif diff --git a/src/dusk/script/core/builtin/math/scriptbmathtrig.h b/src/dusk/script/core/builtin/math/scriptbmathtrig.h new file mode 100644 index 00000000..3bd2f15d --- /dev/null +++ b/src/dusk/script/core/builtin/math/scriptbmathtrig.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_MATH_SCRIPTBMATHTRIG_H +#define SCRIPT_BUILTIN_MATH_SCRIPTBMATHTRIG_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBSin); + +SCRIPT_NATIVE(scriptBCos); + +SCRIPT_NATIVE(scriptBTan); + +SCRIPT_NATIVE(scriptBAtan2); + +#endif diff --git a/src/dusk/script/core/builtin/scriptarg.c b/src/dusk/script/core/builtin/scriptarg.c new file mode 100644 index 00000000..4ccbfc74 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptarg.c @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "array/scriptarraymodify.h" +#include "scripttypes.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" +#include + +void scriptArgCountFail(scriptruntime_t *runtime, const uint8_t argc, const uint8_t minimum, const uint8_t maximum) { + if(minimum == maximum) { + scriptRaise(runtime, "Expected %d arguments, got %d", (int)minimum, (int)argc); + } + scriptRaise(runtime, "Expected %d to %d arguments, got %d", (int)minimum, (int)maximum, (int)argc); +} + +// What a native is given when it was given nothing for an argument +static const scriptvalue_t SCRIPT_ARG_UNDEFINED = { .type = SCRIPT_VALUE_UNDEFINED }; + +// The argument itself, or undefined, with no copy of it: copying a value is a call on the +// Super Nintendo, and what is checked here is mostly its type +static const scriptvalue_t *scriptArgPointer(const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + return index < argc ? &args[index] : &SCRIPT_ARG_UNDEFINED; +} + +scriptvaluetype_t scriptArgType(const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + return scriptArgPointer(args, argc, index)->type; +} + +scriptvalue_t scriptArgOptional(const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + return index < argc ? args[index] : scriptUndefined(); +} + +scriptarray_t *scriptArgArray(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + const scriptvalue_t *value = scriptArgPointer(args, argc, index); + if(value->type != SCRIPT_VALUE_ARRAY) { + scriptRaise(runtime, "Argument %d has to be an array, got %s", (int)index + 1, scriptTypeName(*value)); + } + return value->as.array; +} + +scriptarray_t *scriptArgArrayWritable(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + scriptarray_t *array = scriptArgArray(runtime, args, argc, index); + if(array->frozen) scriptRaise(runtime, "Can not change a constant"); + return array; +} + +scriptstring_t *scriptArgString(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + const scriptvalue_t *value = scriptArgPointer(args, argc, index); + if(value->type != SCRIPT_VALUE_STRING) { + scriptRaise(runtime, "Argument %d has to be a string, got %s", (int)index + 1, scriptTypeName(*value)); + } + return value->as.string; +} + +scriptvalue_t scriptArgNumber(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + const scriptvalue_t *value = scriptArgPointer(args, argc, index); + if(!scriptIsNumber(*value)) { + scriptRaise(runtime, "Argument %d has to be a number, got %s", (int)index + 1, scriptTypeName(*value)); + } + return *value; +} + +int32_t scriptArgInteger(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + const scriptvalue_t *value = scriptArgPointer(args, argc, index); + if(value->type == SCRIPT_VALUE_NUMBER && value->as.number == floorf(value->as.number)) { + return (int32_t)value->as.number; + } + scriptRaise(runtime, "Argument %d has to be a whole number, got %s", (int)index + 1, scriptTypeName(*value)); + return 0; +} + +scriptvalue_t scriptArgFunction(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index) { + const scriptvalue_t *value = scriptArgPointer(args, argc, index); + if(value->type != SCRIPT_VALUE_FUNCTION) { + scriptRaise(runtime, "Argument %d has to be a function, got %s", (int)index + 1, scriptTypeName(*value)); + } + return *value; +} diff --git a/src/dusk/script/core/builtin/scriptarg.h b/src/dusk/script/core/builtin/scriptarg.h new file mode 100644 index 00000000..a1b49852 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptarg.h @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_SCRIPTARG_H +#define SCRIPT_BUILTIN_SCRIPTARG_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +/** + * Says a native got the wrong number of arguments, which is a fatal error. + * + * @param runtime The runtime. + * @param argc Number of arguments given. + * @param minimum Fewest allowed. + * @param maximum Most allowed. + */ +void scriptArgCountFail(scriptruntime_t *runtime, const uint8_t argc, const uint8_t minimum, const uint8_t maximum); + +/** + * Checks a native got between a minimum and maximum number of arguments. The check is made + * here, in the native, as a call is slow on the Super Nintendo and nearly always has nothing + * to say. The arguments are used twice, so they are names or numbers. + * + * @param runtime The runtime. + * @param argc Number of arguments given. + * @param minimum Fewest allowed. + * @param maximum Most allowed. + */ +#define scriptArgCount(runtime, argc, minimum, maximum) \ + ((argc) >= (minimum) && (argc) <= (maximum) ? (void)0 : scriptArgCountFail((runtime), (argc), (minimum), (maximum))) + +/** + * The type of an argument, undefined when it was not given. A native that only wants to + * know what it was given should use this, which does not copy the value. + * + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The type. + */ +scriptvaluetype_t scriptArgType(const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument, undefined when it was not given. + * + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The argument or undefined. + */ +scriptvalue_t scriptArgOptional(const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument that has to be an array. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The array. + */ +/** + * Like scriptArgArray, for a function that changes the array: a constant is not changed. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc How many there are. + * @param index The argument. + * @returns The array. + */ +scriptarray_t *scriptArgArrayWritable(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +scriptarray_t *scriptArgArray(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument that has to be a string. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The string. + */ +scriptstring_t *scriptArgString(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument that has to be a number of any type. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The number value. + */ +scriptvalue_t scriptArgNumber(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument that has to be a whole number: a number with no fraction. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The number, truncated to an int32. + */ +int32_t scriptArgInteger(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +/** + * Gets an argument that has to be a function. + * + * @param runtime The runtime. + * @param args The arguments. + * @param argc Number of arguments. + * @param index Which argument. + * @returns The function value. + */ +scriptvalue_t scriptArgFunction(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, const uint8_t index); + +#endif diff --git a/src/dusk/script/core/builtin/scriptbuiltin.h b/src/dusk/script/core/builtin/scriptbuiltin.h new file mode 100644 index 00000000..48009af9 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptbuiltin.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_SCRIPTBUILTIN_H +#define SCRIPT_BUILTIN_SCRIPTBUILTIN_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" + +// Every built in function has this shape, runtime and arguments in, a value out. +#define SCRIPT_NATIVE(name) scriptvalue_t name( \ + scriptruntime_t *runtime, \ + const scriptvalue_t *args, \ + const uint8_t argc, \ + scriptfunction_t *self \ +) + +#endif diff --git a/src/dusk/script/core/builtin/scriptbuiltinregister.c b/src/dusk/script/core/builtin/scriptbuiltinregister.c new file mode 100644 index 00000000..b5beb80d --- /dev/null +++ b/src/dusk/script/core/builtin/scriptbuiltinregister.c @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "value/scriptvalueprimitive.h" +#include "builtin/scriptbuiltinregister.h" +#include "value/scriptalloc.h" +#include "builtin/core/scriptbinclude.h" +#include "builtin/math/scriptbmathbasic.h" +#include "builtin/core/scriptbpromise.h" +#include "builtin/core/scriptbprint.h" +#include "builtin/math/scriptbmathtrig.h" +#include "builtin/core/scriptbconvert.h" +#include "builtin/math/scriptbmathround.h" +#include "builtin/string/scriptbstringcase.h" +#include "builtin/array/scriptbarraybasic.h" +#include "builtin/string/scriptbstringsearch.h" +#include "builtin/array/scriptbarraymodify.h" +#include "builtin/array/scriptbarraysearch.h" +#include "builtin/array/scriptbarrayslice.h" +#include "builtin/core/scriptbtype.h" +#include "builtin/string/scriptbstringtrim.h" +#include "builtin/array/scriptbarrayfunc.h" +#include "builtin/math/scriptbmathrandom.h" +#include "builtin/array/scriptbarrayorder.h" +#include "builtin/string/scriptbstringsplit.h" +#include "value/scriptfunction.h" +#include "scope/scriptscopeset.h" +#include "assert/assert.h" +#include + +// The built in functions are function objects that are made when the program is, not +// when a runtime is made, so there is none of them to allocate, to walk when the +// collector runs or to throw away. They never change, and the collector does not know +// them for a block of the heap, so it leaves them be. +#if SCRIPT_FEATURE_PRINT +#define SCRIPT_BUILTIN_PRINT(X) X(print, scriptBPrint) +#else +#define SCRIPT_BUILTIN_PRINT(X) +#endif + +#define SCRIPT_BUILTIN_LIST(X) \ + SCRIPT_BUILTIN_PRINT(X) \ + X(assert, scriptBAssert) \ + X(isnull, scriptBIsNull) \ + X(isundefined, scriptBIsUndefined) \ + X(isnullorundefined, scriptBIsNullOrUndefined) \ + X(isfunction, scriptBIsFunction) \ + X(isfunctionasync, scriptBIsFunctionAsync) \ + X(tostr, scriptBToStr) \ + X(tobase, scriptBToBase) \ + X(tonum, scriptBToNum) \ + X(tobool, scriptBToBool) \ + X(promise, scriptBPromise) \ + X(all, scriptBAll) \ + X(any, scriptBAny) \ + X(first, scriptBFirst) \ + X(wait, scriptBWait) \ + X(include, scriptBInclude) \ + X(ainclude, scriptBAInclude) \ + X(abs, scriptBAbs) \ + X(pow, scriptBPow) \ + X(floor, scriptBFloor) \ + X(ceil, scriptBCeil) \ + X(round, scriptBRound) \ + X(sign, scriptBSign) \ + X(min, scriptBMin) \ + X(max, scriptBMax) \ + X(clamp, scriptBClamp) \ + X(rand, scriptBRand) \ + X(randf, scriptBRandf) \ + X(randseed, scriptBRandSeed) \ + X(strindex, scriptBStrIndex) \ + X(contains, scriptBContains) \ + X(startswith, scriptBStartsWith) \ + X(endswith, scriptBEndsWith) \ + X(chr, scriptBChr) \ + X(stringequals, scriptBStringEquals) \ + X(stringequalsinsensitive, scriptBStringEqualsInsensitive) \ + X(ltrim, scriptBLtrim) \ + X(rtrim, scriptBRtrim) \ + X(trim, scriptBTrim) \ + X(downcase, scriptBDowncase) \ + X(upcase, scriptBUpcase) \ + X(split, scriptBSplit) \ + X(len, scriptBLen) \ + X(push, scriptBPush) \ + X(pop, scriptBPop) \ + X(shift, scriptBShift) \ + X(unshift, scriptBUnshift) \ + X(fill, scriptBFill) \ + X(filter, scriptBFilter) \ + X(merge, scriptBMerge) \ + X(splice, scriptBSplice) \ + X(insert, scriptBInsert) \ + X(insertarr, scriptBInsertArr) \ + X(indexof, scriptBIndexOf) \ + X(lastindexof, scriptBLastIndexOf) \ + X(sort, scriptBSort) \ + X(reverse, scriptBReverse) \ + X(shuffle, scriptBShuffle) \ + X(empty, scriptBEmpty) \ + X(clone, scriptBClone) \ + X(subarr, scriptBSubarr) \ + X(deepclone, scriptBDeepClone) \ + X(deepequal, scriptBDeepEqual) \ + X(map, scriptBMap) \ + X(reduce, scriptBReduce) \ + X(flatten, scriptBFlatten) \ + X(join, scriptBJoin) + +// The ones that need the maths library, which not every platform has +#if SCRIPT_FEATURE_MATH +#define SCRIPT_BUILTIN_MATH_LIST(X) \ + X(sin, scriptBSin) \ + X(cos, scriptBCos) \ + X(tan, scriptBTan) \ + X(atan2, scriptBAtan2) \ + X(sqrt, scriptBSqrt) +#else +#define SCRIPT_BUILTIN_MATH_LIST(X) +#endif +#define SCRIPT_BUILTIN_ALL(X) SCRIPT_BUILTIN_LIST(X) SCRIPT_BUILTIN_MATH_LIST(X) + +// One function object for each of them, and one variable of the built in scope for each, +// that is copied into the scope of a runtime in one go when it is made. +#define SCRIPT_BUILTIN_FUNCTION(id, fn) { .name = #id, .native = fn }, +static const scriptfunction_t SCRIPT_BUILTINS[] = { + SCRIPT_BUILTIN_ALL(SCRIPT_BUILTIN_FUNCTION) +}; + +#define SCRIPT_BUILTIN_INDEX(id, fn) SCRIPT_BUILTIN_INDEX_##id, +enum { + SCRIPT_BUILTIN_ALL(SCRIPT_BUILTIN_INDEX) + SCRIPT_BUILTIN_COUNT +}; + +#define SCRIPT_BUILTIN_VARIABLE(id, fn) { .name = #id, .value = { .type = SCRIPT_VALUE_FUNCTION, .as = { .function = (scriptfunction_t *)&SCRIPT_BUILTINS[SCRIPT_BUILTIN_INDEX_##id] } }, .declared = 1 }, +static const scriptvariable_t SCRIPT_BUILTIN_VARIABLES[] = { + SCRIPT_BUILTIN_ALL(SCRIPT_BUILTIN_VARIABLE) +}; + + +void scriptBuiltinAdd(scriptruntime_t *runtime, const char *name, scriptnative_t native) { + assertNotNull(runtime, "scriptBuiltinAdd needs a runtime, not NULL"); + assertNotNull(name, "scriptBuiltinAdd needs a name, not NULL"); + assertNotNull(native, "scriptBuiltinAdd needs a native function, not NULL"); + scriptScopeAdd(runtime, runtime->builtinScope, name, scriptFunctionNative(runtime, name, native, NULL)); +} + +void scriptBuiltinAddInt(scriptruntime_t *runtime, const char *name, const int16_t value) { + assertNotNull(runtime, "scriptBuiltinAddInt needs a runtime, not NULL"); + assertNotNull(name, "scriptBuiltinAddInt needs a name, not NULL"); + scriptScopeAdd(runtime, runtime->builtinScope, name, scriptNumber((float)value)); +} + +void scriptBuiltinsRegister(scriptruntime_t *runtime) { + scriptscope_t *scope; + assertNotNull(runtime, "scriptBuiltinsRegister needs a runtime, not NULL"); + scope = runtime->builtinScope; + + // The scope gets all it will hold at once, and the built in functions go in as they + // are in the program, adding them one at a time takes more than the rest of starting + scriptScopeReserve(runtime, scope, (uint16_t)(SCRIPT_BUILTIN_COUNT + 1 + SCRIPT_BUILTINS_SPARE)); + memoryCopy(scope->variables, SCRIPT_BUILTIN_VARIABLES, sizeof(SCRIPT_BUILTIN_VARIABLES)); + scope->count = SCRIPT_BUILTIN_COUNT; + scriptScopeAdd(runtime, scope, "PI", scriptNumber(3.14159265358979323846f)); +} diff --git a/src/dusk/script/core/builtin/scriptbuiltinregister.h b/src/dusk/script/core/builtin/scriptbuiltinregister.h new file mode 100644 index 00000000..6bd7bc78 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptbuiltinregister.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_SCRIPTBUILTINREGISTER_H +#define SCRIPT_BUILTIN_SCRIPTBUILTINREGISTER_H + +#include "core/scriptcore.h" +#include "value/scriptfunction.h" + +/** + * Adds one built in function to the builtin scope. + * + * @param runtime The runtime. + * @param name The function name. + * @param native The C function. + */ +void scriptBuiltinAdd(scriptruntime_t *runtime, const char *name, scriptnative_t native); + +/** + * Adds a whole number to the builtin scope, which a script can read but not declare again. + * For what the game has that scripts name: the scenes, say. + * + * @param runtime The runtime. + * @param name The constant's name. + * @param value The number. + */ +void scriptBuiltinAddInt(scriptruntime_t *runtime, const char *name, const int16_t value); + +/** + * Puts all the built in functions and constants in the builtin scope. + * + * @param runtime The runtime. + */ +void scriptBuiltinsRegister(scriptruntime_t *runtime); + +#endif diff --git a/src/dusk/script/core/builtin/scriptintrinsic.c b/src/dusk/script/core/builtin/scriptintrinsic.c new file mode 100644 index 00000000..14c615a8 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptintrinsic.c @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "builtin/scriptintrinsic.h" +#include "builtin/core/scriptbtype.h" +#include "builtin/array/scriptbarraybasic.h" +#include "builtin/math/scriptbmathbasic.h" +#include "coroutine/scriptcoroutine.h" +#include + +typedef scriptvalue_t (*scriptintrinsicfunction_t)(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, scriptfunction_t *self); + +static const struct { + const char *name; + scriptintrinsicfunction_t function; +} SCRIPT_INTRINSICS[SCRIPT_INTRINSIC_COUNT] = { + { "isnull", scriptBIsNull }, + { "isundefined", scriptBIsUndefined }, + { "isnullorundefined", scriptBIsNullOrUndefined }, + { "isfunction", scriptBIsFunction }, + { "len", scriptBLen }, + { "abs", scriptBAbs }, + { "isfunctionasync", scriptBIsFunctionAsync } +}; + +const char *scriptIntrinsicName(const scriptintrinsic_t id) { + return SCRIPT_INTRINSICS[id].name; +} + +scriptvalue_t scriptIntrinsicCall(scriptruntime_t *runtime, const scriptintrinsic_t id, const scriptvalue_t *value) { + scriptcoroutine_t *co = runtime->current; + const char *saved = co->nativeName; + scriptvalue_t result; + + // An error is the function's, as it is in a call of it + co->nativeName = SCRIPT_INTRINSICS[id].name; + result = SCRIPT_INTRINSICS[id].function(runtime, value, 1, NULL); + co->nativeName = saved; + return result; +} + +scriptintrinsic_t scriptIntrinsicFind(const char *name, const size_t length) { + uint8_t i; + for(i = 0; i < SCRIPT_INTRINSIC_COUNT; i++) { + if(strlen(SCRIPT_INTRINSICS[i].name) == length && memoryCompare(SCRIPT_INTRINSICS[i].name, name, length) == 0) return (scriptintrinsic_t)i; + } + return SCRIPT_INTRINSIC_COUNT; +} diff --git a/src/dusk/script/core/builtin/scriptintrinsic.h b/src/dusk/script/core/builtin/scriptintrinsic.h new file mode 100644 index 00000000..c78e855e --- /dev/null +++ b/src/dusk/script/core/builtin/scriptintrinsic.h @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +// The built in functions that take one value and say something of it, which the compiler turns into one +// instruction (INTRINSIC) in place of a call of the function, when the name is the built in function and the call has the +// one argument. The numbers are in the instruction, and in the assembly of the Super Nintendo. +typedef enum { + SCRIPT_INTRINSIC_ISNULL, + SCRIPT_INTRINSIC_ISUNDEFINED, + SCRIPT_INTRINSIC_ISNULLORUNDEFINED, + SCRIPT_INTRINSIC_ISFUNCTION, + SCRIPT_INTRINSIC_LEN, + SCRIPT_INTRINSIC_ABS, + SCRIPT_INTRINSIC_ISFUNCTIONASYNC, + SCRIPT_INTRINSIC_COUNT +} scriptintrinsic_t; + +/** + * The name of an intrinsic, which is the name of the built in function. + * + * @param id The intrinsic. + * @returns The name. + */ +const char *scriptIntrinsicName(const scriptintrinsic_t id); + +/** + * The built in function an intrinsic is, which does what the instruction does for what it has no quick way of doing, + * and says what is wrong with it. + * + * @param runtime The runtime. + * @param id The intrinsic. + * @param value The value it is called with. + * @returns The function. + */ +scriptvalue_t scriptIntrinsicCall(scriptruntime_t *runtime, const scriptintrinsic_t id, const scriptvalue_t *value); + +/** + * Which intrinsic a name is. + * + * @param name The name. + * @param length Length of the name. + * @returns The intrinsic, SCRIPT_INTRINSIC_COUNT when the name is not one. + */ +scriptintrinsic_t scriptIntrinsicFind(const char *name, const size_t length); diff --git a/src/dusk/script/core/builtin/scriptnativesig.c b/src/dusk/script/core/builtin/scriptnativesig.c new file mode 100644 index 00000000..274e0390 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptnativesig.c @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/scriptnativesig.h" +#include "builtin/scriptarg.h" +#include "builtin/scriptbuiltinregister.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "scope/scriptscopeset.h" +#include "runtime/scriptruntimestate.h" + +// What an argument is said to have to be, as the natives that check it themselves say it +static const char *scriptNativeTypesName(const uint16_t types) { + if(types == SCRIPT_TYPES_NUMBER) return "a number"; + if(types == SCRIPT_TYPE_BIT(SCRIPT_VALUE_STRING)) return "a string"; + if(types == SCRIPT_TYPE_BIT(SCRIPT_VALUE_ARRAY)) return "an array"; + if(types == SCRIPT_TYPE_BIT(SCRIPT_VALUE_FUNCTION)) return "a function"; + if(types == SCRIPT_TYPE_BIT(SCRIPT_VALUE_BOOL)) return "a boolean"; + return "a different kind of value"; +} + +void scriptNativeCheckArgs(scriptruntime_t *runtime, const scriptnativesig_t *signature, const scriptvalue_t *args, const uint8_t mask) { + uint8_t i; + + for(i = 0; i < signature->argMax; i++) { + if(!(mask & (1u << i))) continue; + if(signature->argTypes[i] & SCRIPT_TYPE_BIT(args[i].type)) continue; + scriptRaise(runtime, "Argument %d has to be %s, got %s", (int)i + 1, scriptNativeTypesName(signature->argTypes[i]), scriptTypeName(args[i])); + } +} + +#if SCRIPT_FEATURE_TRUST_BYTECODE +// A native of a platform that trusts the bytecode, called by its name: nothing has looked at +// what it was given, so it is all looked at here, and what was left out is filled in +static scriptvalue_t scriptNativeCheckedCall(scriptruntime_t *runtime, const scriptvalue_t *args, const uint8_t argc, scriptfunction_t *self) { + const scriptnativeentry_t *entry = (const scriptnativeentry_t *)self->data; + const scriptnativesig_t *signature = entry->signature; + scriptvalue_t full[SCRIPT_PARAMS_MAX]; + uint8_t i; + + scriptArgCount(runtime, argc, signature->argMin, signature->argMax); + for(i = 0; i < signature->argMax; i++) { + if(i < argc) { + full[i] = args[i]; + } else { + full[i].type = signature->defaults[i].type; + full[i].as.raw = signature->defaults[i].value; + } + } + scriptNativeCheckArgs(runtime, signature, full, (uint8_t)((1u << signature->argMax) - 1)); + return entry->function(runtime, full, signature->argMax, self); +} +#endif + +void scriptNativesAdd(scriptruntime_t *runtime, const scriptnativeentry_t *entries, const uint16_t count) { + uint16_t i; + + runtime->nativeEntries = entries; + runtime->nativeCount = count; + for(i = 0; i < count; i++) { +#if SCRIPT_FEATURE_TRUST_BYTECODE + scriptScopeAdd(runtime, runtime->builtinScope, entries[i].name, scriptFunctionNative(runtime, entries[i].name, scriptNativeCheckedCall, (void *)&entries[i])); +#else + scriptBuiltinAdd(runtime, entries[i].name, entries[i].function); +#endif + } +} diff --git a/src/dusk/script/core/builtin/scriptnativesig.h b/src/dusk/script/core/builtin/scriptnativesig.h new file mode 100644 index 00000000..935887e4 --- /dev/null +++ b/src/dusk/script/core/builtin/scriptnativesig.h @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_SCRIPTNATIVESIG_H +#define SCRIPT_BUILTIN_SCRIPTNATIVESIG_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" +#include "value/scriptfunction.h" +#include "builtin/scriptbuiltin.h" +#include "builtin/scriptarg.h" + +// What a native takes and gives back, written in natives.json and made into the tables of this +// file by tools/duskscriptnatives. The compiler uses them to call a native by its number, with +// the arguments it can check checked, and the machine checks the others. + +// A set of types, a bit for each of scriptvaluetype_t +#define SCRIPT_TYPE_BIT(type) ((uint16_t)(1u << (type))) +#define SCRIPT_TYPES_NUMBER SCRIPT_TYPE_BIT(SCRIPT_VALUE_NUMBER) + +// What an argument is when it is left out, which an optional argument can be. For a +// number, value holds the bits of the float, the same way a PUSH_NUMBER constant does. +typedef struct { + scriptvaluetype_t type; + int32_t value; +} scriptnativedefault_t; + +typedef struct { + const char *name; + uint8_t argMin; + uint8_t argMax; + // The types each argument can be, and what is used for the optional ones that are left out + uint16_t argTypes[SCRIPT_PARAMS_MAX]; + scriptnativedefault_t defaults[SCRIPT_PARAMS_MAX]; + // The types it can give back, which nothing checks yet, it is what the compiler can know + uint16_t returnTypes; +} scriptnativesig_t; + +// A value the game gives a script a name for, which the compiler knows the type and the value of, +// so a name that nothing in the script changes is compiled to the number +typedef struct { + const char *name; + scriptvaluetype_t type; + int32_t value; +} scriptnativeconstant_t; + +typedef struct { + const scriptnativesig_t *signatures; + uint16_t count; + const scriptnativeconstant_t *constants; + uint16_t constantCount; +} scriptnativetable_t; + +// What the machine finds a native by, for the instruction that calls one by its number. 16 +// bytes each on the Super Nintendo, which the assembly counts on. +typedef struct scriptnativeentry_t { + scriptnative_t function; + const char *name; + const scriptnativesig_t *signature; + uint32_t reserved; +} scriptnativeentry_t; + +// The table the build makes from natives.json, which the compiler is given +extern const scriptnativetable_t SCRIPT_NATIVE_TABLE; + +/** + * Makes the natives of a table known to a runtime: they can be called by their number and by + * their name, which is what a script calls them by when the compiler could not use the number. + * On a platform that trusts the bytecode, the name finds a function that checks everything + * first, as the native itself does not. + * + * @param runtime The runtime. + * @param entries The natives, in the order of the table. + * @param count How many there are. + */ +void scriptNativesAdd(scriptruntime_t *runtime, const scriptnativeentry_t *entries, const uint16_t count); + +/** + * Checks the arguments of a call against the signature, which says that the call is wrong + * when it is. Does the arguments that are in the mask, a bit for each. + * + * @param runtime The runtime. + * @param signature What the native takes. + * @param args The arguments. + * @param mask The arguments to check. + */ +void scriptNativeCheckArgs(scriptruntime_t *runtime, const scriptnativesig_t *signature, const scriptvalue_t *args, const uint8_t mask); + +// The number of arguments of a native with a signature. When the bytecode is trusted it is the +// number the signature says, and when it is not, it is counted here. +#if SCRIPT_FEATURE_TRUST_BYTECODE +#define SCRIPT_NATIVE_ARGC(runtime, argc, minimum, maximum) ((void)0) +#else +#define SCRIPT_NATIVE_ARGC(runtime, argc, minimum, maximum) scriptArgCount((runtime), (argc), (minimum), (maximum)) +#endif + +// A whole number argument of a native with a signature. When the bytecode is trusted the +// compiler or the machine has seen it is one, and when it is not, it is looked at here. +#if SCRIPT_FEATURE_TRUST_BYTECODE +#define SCRIPT_NATIVE_INT(runtime, args, argc, index) ((int32_t)(args)[(index)].as.number) +#else +#define SCRIPT_NATIVE_INT(runtime, args, argc, index) scriptArgInteger((runtime), (args), (argc), (index)) +#endif + +#endif diff --git a/src/dusk/script/core/builtin/string/scriptbstringcase.c b/src/dusk/script/core/builtin/string/scriptbstringcase.c new file mode 100644 index 00000000..caba229a --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringcase.c @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/string/scriptbstringcase.h" +#include "builtin/scriptarg.h" +#include "string/scriptstringchar.h" +#include "error/scriptraise.h" +#include "string/scriptstringnew.h" +#include "value/scriptfunction.h" + +SCRIPT_NATIVE(scriptBChr) { + int32_t code; + char c; + scriptArgCount(runtime, argc, 1, 1); + code = scriptArgInteger(runtime, args, argc, 0); + if(code < 1 || code > 255) scriptRaise(runtime, "chr needs a char code from 1 to 255, got %d", (int)code); + c = (char)code; + return scriptStringNew(runtime, &c, 1); +} + +SCRIPT_NATIVE(scriptBDowncase) { + scriptstring_t *text; + scriptvalue_t result; + size_t i; + scriptArgCount(runtime, argc, 1, 1); + text = scriptArgString(runtime, args, argc, 0); + result = scriptStringNew(runtime, text->data, text->length); + for(i = 0; i < text->length; i++) result.as.string->data[i] = scriptCharLower(text->data[i]); + return result; +} + +SCRIPT_NATIVE(scriptBUpcase) { + scriptstring_t *text; + scriptvalue_t result; + size_t i; + scriptArgCount(runtime, argc, 1, 1); + text = scriptArgString(runtime, args, argc, 0); + result = scriptStringNew(runtime, text->data, text->length); + for(i = 0; i < text->length; i++) result.as.string->data[i] = scriptCharUpper(text->data[i]); + return result; +} diff --git a/src/dusk/script/core/builtin/string/scriptbstringcase.h b/src/dusk/script/core/builtin/string/scriptbstringcase.h new file mode 100644 index 00000000..f64814a5 --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringcase.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGCASE_H +#define SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGCASE_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBChr); + +SCRIPT_NATIVE(scriptBDowncase); + +SCRIPT_NATIVE(scriptBUpcase); + +#endif diff --git a/src/dusk/script/core/builtin/string/scriptbstringsearch.c b/src/dusk/script/core/builtin/string/scriptbstringsearch.c new file mode 100644 index 00000000..b65242ed --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringsearch.c @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "builtin/string/scriptbstringsearch.h" +#include "builtin/scriptarg.h" +#include "value/scriptvalueprimitive.h" +#include "string/scriptstringchar.h" +#include "string/scriptstringsearch.h" +#include + +SCRIPT_NATIVE(scriptBStrIndex) { + scriptArgCount(runtime, argc, 2, 2); + return scriptNumber((float)scriptStringSearch(scriptArgString(runtime, args, argc, 0), scriptArgString(runtime, args, argc, 1))); +} + +SCRIPT_NATIVE(scriptBContains) { + scriptArgCount(runtime, argc, 2, 2); + return scriptBool(scriptStringSearch(scriptArgString(runtime, args, argc, 0), scriptArgString(runtime, args, argc, 1)) != -1); +} + +SCRIPT_NATIVE(scriptBStartsWith) { + scriptstring_t *text; + scriptstring_t *piece; + scriptArgCount(runtime, argc, 2, 2); + text = scriptArgString(runtime, args, argc, 0); + piece = scriptArgString(runtime, args, argc, 1); + return scriptBool(piece->length <= text->length && memoryCompare(text->data, piece->data, piece->length) == 0); +} + +SCRIPT_NATIVE(scriptBEndsWith) { + scriptstring_t *text; + scriptstring_t *piece; + scriptArgCount(runtime, argc, 2, 2); + text = scriptArgString(runtime, args, argc, 0); + piece = scriptArgString(runtime, args, argc, 1); + return scriptBool( + piece->length <= text->length && + memoryCompare(text->data + text->length - piece->length, piece->data, piece->length) == 0 + ); +} + +SCRIPT_NATIVE(scriptBStringEquals) { + scriptstring_t *a; + scriptstring_t *b; + scriptArgCount(runtime, argc, 2, 2); + a = scriptArgString(runtime, args, argc, 0); + b = scriptArgString(runtime, args, argc, 1); + return scriptBool(a->length == b->length && memoryCompare(a->data, b->data, a->length) == 0); +} + +SCRIPT_NATIVE(scriptBStringEqualsInsensitive) { + scriptstring_t *a; + scriptstring_t *b; + size_t i; + scriptArgCount(runtime, argc, 2, 2); + a = scriptArgString(runtime, args, argc, 0); + b = scriptArgString(runtime, args, argc, 1); + if(a->length != b->length) return scriptBool(false); + for(i = 0; i < a->length; i++) { + if(scriptCharLower(a->data[i]) != scriptCharLower(b->data[i])) return scriptBool(false); + } + return scriptBool(true); +} diff --git a/src/dusk/script/core/builtin/string/scriptbstringsearch.h b/src/dusk/script/core/builtin/string/scriptbstringsearch.h new file mode 100644 index 00000000..026f7728 --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringsearch.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGSEARCH_H +#define SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGSEARCH_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +// Strings +SCRIPT_NATIVE(scriptBStrIndex); + +SCRIPT_NATIVE(scriptBContains); + +SCRIPT_NATIVE(scriptBStartsWith); + +SCRIPT_NATIVE(scriptBEndsWith); + +SCRIPT_NATIVE(scriptBStringEquals); + +SCRIPT_NATIVE(scriptBStringEqualsInsensitive); + +#endif diff --git a/src/dusk/script/core/builtin/string/scriptbstringsplit.c b/src/dusk/script/core/builtin/string/scriptbstringsplit.c new file mode 100644 index 00000000..072386ab --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringsplit.c @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "util/memory.h" +#include "scripttypes.h" +#include "builtin/string/scriptbstringsplit.h" +#include "builtin/scriptarg.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "string/scriptstringnew.h" +#include "value/scriptfunction.h" +#include + +SCRIPT_NATIVE(scriptBSplit) { + scriptstring_t *text; + scriptstring_t *separator; + scriptvalue_t result = scriptArrayNew(runtime); + size_t start = 0; + size_t i; + + scriptArgCount(runtime, argc, 2, 2); + text = scriptArgString(runtime, args, argc, 0); + separator = scriptArgString(runtime, args, argc, 1); + + // An empty separator splits into single chars + if(separator->length == 0) { + for(i = 0; i < text->length; i++) scriptArrayPush(runtime, result.as.array, scriptStringNew(runtime, text->data + i, 1)); + return result; + } + + for(i = 0; i + separator->length <= text->length;) { + if(memoryCompare(text->data + i, separator->data, separator->length) == 0) { + scriptArrayPush(runtime, result.as.array, scriptStringNew(runtime, text->data + start, i - start)); + i += separator->length; + start = i; + } else { + i++; + } + } + scriptArrayPush(runtime, result.as.array, scriptStringNew(runtime, text->data + start, text->length - start)); + return result; +} diff --git a/src/dusk/script/core/builtin/string/scriptbstringsplit.h b/src/dusk/script/core/builtin/string/scriptbstringsplit.h new file mode 100644 index 00000000..aeb1815e --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringsplit.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGSPLIT_H +#define SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGSPLIT_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBSplit); + +#endif diff --git a/src/dusk/script/core/builtin/string/scriptbstringtrim.c b/src/dusk/script/core/builtin/string/scriptbstringtrim.c new file mode 100644 index 00000000..29a86a62 --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringtrim.c @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "builtin/string/scriptbstringtrim.h" +#include "builtin/scriptarg.h" +#include "string/scriptstringchar.h" + +SCRIPT_NATIVE(scriptBLtrim) { + scriptArgCount(runtime, argc, 1, 1); + return scriptStringTrim(runtime, scriptArgString(runtime, args, argc, 0), true, false); +} + +SCRIPT_NATIVE(scriptBRtrim) { + scriptArgCount(runtime, argc, 1, 1); + return scriptStringTrim(runtime, scriptArgString(runtime, args, argc, 0), false, true); +} + +SCRIPT_NATIVE(scriptBTrim) { + scriptArgCount(runtime, argc, 1, 1); + return scriptStringTrim(runtime, scriptArgString(runtime, args, argc, 0), true, true); +} diff --git a/src/dusk/script/core/builtin/string/scriptbstringtrim.h b/src/dusk/script/core/builtin/string/scriptbstringtrim.h new file mode 100644 index 00000000..9a32a7d6 --- /dev/null +++ b/src/dusk/script/core/builtin/string/scriptbstringtrim.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGTRIM_H +#define SCRIPT_BUILTIN_STRING_SCRIPTBSTRINGTRIM_H + +#include "core/scriptcore.h" +#include "builtin/scriptbuiltin.h" + +SCRIPT_NATIVE(scriptBLtrim); + +SCRIPT_NATIVE(scriptBRtrim); + +SCRIPT_NATIVE(scriptBTrim); + +#endif diff --git a/src/dusk/script/core/bytecode/scriptbytecode.h b/src/dusk/script/core/bytecode/scriptbytecode.h new file mode 100644 index 00000000..3aab6d7a --- /dev/null +++ b/src/dusk/script/core/bytecode/scriptbytecode.h @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BYTECODE_SCRIPTBYTECODE_H +#define SCRIPT_BYTECODE_SCRIPTBYTECODE_H + +#include "core/scriptcore.h" + +// A compiled script, a .dsb file, is laid out like this. Numbers are little +// endian and nothing needs aligning. +// +// "DSB" magic +// u8 version SCRIPT_BINARY_VERSION +// u16 stringCount +// u16 functionCount function 0 is the script itself +// u16 codeSize +// strings u16 length, the bytes, a zero byte +// functions u8 flags, u8 paramCount, u8 localCount, u8 stackMax, +// u16 bodyOffset, then for each parameter u16 defaultOffset +// code the instructions +// +// What is only for errors is in the map of the script, see SCRIPT_MAP_VERSION and scriptBuilderMap: +// +// "DSM" magic +// u8 version SCRIPT_MAP_VERSION +// u16 codeSize, u16 check of the code it is for, a map of other code is not used +// u16 functionCount +// u16 positionSize +// source the file the script was compiled from, u16 length, the bytes, a zero byte +// functions the name as the source is, u16 line, u16 column, u8 localCount, then the name of +// each variable, the parameters first +// positions where in the source the code is, see below +// +// The variables of a function are numbered, and they live in a block of values +// next to its stack. The parameters are the first ones. The names are only there +// for error messages. The exception is the variables at the top level of the +// script itself, and the names a function does not have a variable for. Those +// are found by name once, when the script loads, and each instruction that has +// one of them has the variable. They are globals, the variables of the script +// itself are the script's own when it is an include. +// +// Positions are only looked at when there is an error. They are a list, in the +// order of the code, of the places where the line and column of the source +// change: a varint for how far into the code it is since the last place, a +// zigzag varint for how the line changed, and a varint for the column. The code +// at an offset has the position of the last place at or before it. +// +// All the functions share one code block, the offsets point into it. The script +// is a stack machine: instructions take their operands from a stack of values +// and leave their result on it. + +#define SCRIPT_BINARY_VERSION 19 + +// The map of a compiled script, a .map file next to the .dsb. It is for errors: the name of the file the +// script was compiled from, the names of the functions and their variables, and where in the source the +// code is. A platform that has no .map has a script that runs the same, and errors that say less. +#define SCRIPT_MAP_VERSION 1 + +// Flags of a function. +#define SCRIPT_BINARY_FLAG_ASYNC 1 + +// An offset or string index that points at nothing, a parameter with no default. +#define SCRIPT_BINARY_NONE 0xFFFF + +typedef enum { + // Constants. + SCRIPT_BC_PUSH_UNDEFINED, + SCRIPT_BC_PUSH_NULL, + SCRIPT_BC_PUSH_TRUE, + SCRIPT_BC_PUSH_FALSE, + SCRIPT_BC_PUSH_INT, // i16, a whole number that fits + SCRIPT_BC_PUSH_NUMBER, // i32, the bits of a float + SCRIPT_BC_PUSH_STRING, // u16 string + + // Names and literals. + SCRIPT_BC_LOAD_VAR, // u16 name, looked up in the scope + SCRIPT_BC_LOAD_LOCAL, // u8 variable + SCRIPT_BC_NEW_ARRAY, // u16 count; the array is made with room for as many items as it is written with + SCRIPT_BC_ARRAY_PUSH, // value, array -> array + SCRIPT_BC_NEW_STRUCT, + SCRIPT_BC_STRUCT_SET, // u16 key; value, struct -> struct + SCRIPT_BC_MAKE_FUNCTION, // u16 function + + // Operators. + SCRIPT_BC_NOT, + SCRIPT_BC_NEGATE, + SCRIPT_BC_INVERT, + SCRIPT_BC_BINARY, // u8 token; a, b -> result + SCRIPT_BC_BINARY_INT, // u8 token, i16 constant; a -> result, a BINARY with a small number on the right + SCRIPT_BC_AND_JUMP, // u16 target; false and jump when the top is falsy + SCRIPT_BC_OR_JUMP, // u16 target; true and jump when the top is truthy + SCRIPT_BC_TO_BOOL, + + // Assignment. The last byte is the token (= or a compound assignment) or the mode, and its top bit, + + // SCRIPT_BC_DISCARD, says nothing is left on the stack, the value is not wanted. + SCRIPT_BC_ASSIGN_VAR, // u16 name, u8 token; value -> value + SCRIPT_BC_ASSIGN_LOCAL, // u8 variable, u8 token; value -> value + SCRIPT_BC_ASSIGN_INDEX, // u8 token; container, key, value -> value + SCRIPT_BC_ASSIGN_MEMBER, // u16 name, u8 token; container, value -> value + SCRIPT_BC_INCDEC_VAR, // u16 name, u8 mode + SCRIPT_BC_INCDEC_LOCAL, // u8 variable, u8 mode + SCRIPT_BC_INCDEC_INDEX, // u8 mode; container, key -> value + SCRIPT_BC_INCDEC_MEMBER, // u16 name, u8 mode; container -> value + + // Calls, a call needs the function, then this, then the arguments. + SCRIPT_BC_CALL, // u8 argc, u16 name of the callee; function, args -> result + SCRIPT_BC_INDEX, + SCRIPT_BC_MEMBER, // u16 name + SCRIPT_BC_AWAIT, + + // Statements. + SCRIPT_BC_POP, + SCRIPT_BC_DECLARE, // u16 name; value ->, in the scope, fails when it is there + SCRIPT_BC_DECLARE_LOCAL, // u8 variable; value -> + SCRIPT_BC_DECLARE_FAIL, // u16 name; value ->, a variable that is declared twice + SCRIPT_BC_JUMP, // u16 target + SCRIPT_BC_JUMP_IF_FALSE, // u16 target; value -> + SCRIPT_BC_JUMP_IF_TRUE, // u16 target; value -> + SCRIPT_BC_COMPARE_JUMP, // u8 token, u16 target; a, b ->, jumps when the comparison is false + SCRIPT_BC_COMPARE_INT_JUMP, // u8 token, i16 constant, u16 target; a ->, jumps when the comparison is false + SCRIPT_BC_COMPARE_LOCAL_INT_JUMP, // u8 variable, u8 token, i16 constant, u16 target; jumps when the comparison of the variable is false + SCRIPT_BC_JUMP_IF_EQUAL, // u16 target; subject, value -> subject, or nothing when it jumps + SCRIPT_BC_FOREACH_BEGIN, + SCRIPT_BC_FOREACH_NEXT, // u8 variable, u16 end + SCRIPT_BC_FUNC_DECL, // u16 name, u16 function + SCRIPT_BC_RETURN, // value -> + SCRIPT_BC_END, // ends a default value, value -> + + // A call of a native with a signature (natives.json) by its number, after the compiler has put + // the arguments on the stack with their defaults, which it saw were what the native takes + // as far as it could, and the machine checks the others. + SCRIPT_BC_CALL_NATIVE, // u8 native, u8 argc, u8 check; placeholder, args -> result, check has a bit for each argument that is looked at + + // Constants (const): mode 0 makes the array or struct on top a constant, which nothing changes after, + // and what it holds that is an array or a struct has to be a constant already. Mode 1 fails when + // the value on top is an array or a struct that is not a constant, for a const set to an expression. + SCRIPT_BC_FREEZE, // u8 mode; value -> value + + // A variable set to itself plus or minus a small number as a statement, x += 1 and x = x - 2, one + // instruction in place of a load, an add and an assignment. The token is + or -. + SCRIPT_BC_ASSIGN_LOCAL_INT, // u8 variable, u8 token, i16 constant + + // A variable and a small number, or two variables, that are an operator's operands: what a load, a load or a + // number and a BINARY (or a comparison and a jump) were. The result is pushed. + SCRIPT_BC_BINARY_LOCAL_INT, // u8 variable, u8 token, i16 constant; -> result + SCRIPT_BC_BINARY_LOCAL_LOCAL, // u8 variable, u8 variable, u8 token; -> result + SCRIPT_BC_COMPARE_LOCAL_LOCAL_JUMP, // u8 variable, u8 variable, u8 token, u16 target; jumps when the comparison is false + + // A call of a built in function that takes one value and says something of it (isnull, len, abs, see scriptintrinsic.h): + // the value is replaced by what it says. + SCRIPT_BC_INTRINSIC, // u8 intrinsic; value -> result + + // A variable set to what an operator makes of a variable and a small number, or of two variables, or to another + // variable, as a statement: what the BINARY_LOCAL_* and an assignment were, with nothing pushed and taken off. + SCRIPT_BC_SET_LOCAL_BINARY_INT, // u8 variable, u8 variable, u8 token, i16 constant + SCRIPT_BC_SET_LOCAL_BINARY_LOCAL, // u8 variable, u8 variable, u8 variable, u8 token + SCRIPT_BC_SET_LOCAL_LOCAL, // u8 variable, u8 variable + + // The value on top and a variable, which is a load and a BINARY: the top is what the operator makes of it and the variable. + SCRIPT_BC_BINARY_TOP_LOCAL, // u8 variable, u8 token; value -> result + + // A member or an item of a variable, which is a load and a MEMBER or an INDEX: pushed. + SCRIPT_BC_MEMBER_LOCAL, // u8 variable, u16 name; -> value + SCRIPT_BC_INDEX_LOCAL_INT, // u8 variable, i16 index; -> value + SCRIPT_BC_INDEX_LOCAL_LOCAL, // u8 variable, u8 variable; -> value + + // A key of a struct that is written out, when no key before it in the struct has the same name: it is added without looking + // for one that is there. + SCRIPT_BC_STRUCT_ADD, // u16 key; value, struct -> struct + + SCRIPT_BC_COUNT +} scriptbytecode_t; + +// Modes of the increment and decrement instructions. +#define SCRIPT_BC_INCDEC_DECREMENT 1 +#define SCRIPT_BC_INCDEC_PREFIX 2 + +// Set in the token or the mode of an assignment or an increment when its value is +// not wanted, which saves putting it on the stack and taking it off again. +#define SCRIPT_BC_DISCARD 0x80 + +// Set in the token or the mode of an assignment or an increment when its value is +// not wanted, which saves putting it on the stack and taking it off again. +#define SCRIPT_BC_DISCARD 0x80 + +#endif diff --git a/src/dusk/script/core/bytecode/scriptbytecodesize.c b/src/dusk/script/core/bytecode/scriptbytecodesize.c new file mode 100644 index 00000000..e1a3182b --- /dev/null +++ b/src/dusk/script/core/bytecode/scriptbytecodesize.c @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "core/scriptcore.h" +#include "bytecode/scriptbytecodesize.h" + +const uint8_t scriptBytecodeSize[SCRIPT_BC_COUNT] = { + 1 /* PUSH_UNDEFINED */, + 1 /* PUSH_NULL */, + 1 /* PUSH_TRUE */, + 1 /* PUSH_FALSE */, + 3 /* PUSH_INT */, + 5 /* PUSH_NUMBER */, + 3 /* PUSH_STRING */, + 3 /* LOAD_VAR */, + 2 /* LOAD_LOCAL */, + 3 /* NEW_ARRAY */, + 1 /* ARRAY_PUSH */, + 1 /* NEW_STRUCT */, + 3 /* STRUCT_SET */, + 3 /* MAKE_FUNCTION */, + 1 /* NOT */, + 1 /* NEGATE */, + 1 /* INVERT */, + 2 /* BINARY */, + 4 /* BINARY_INT */, + 3 /* AND_JUMP */, + 3 /* OR_JUMP */, + 1 /* TO_BOOL */, + 4 /* ASSIGN_VAR */, + 3 /* ASSIGN_LOCAL */, + 2 /* ASSIGN_INDEX */, + 4 /* ASSIGN_MEMBER */, + 4 /* INCDEC_VAR */, + 3 /* INCDEC_LOCAL */, + 2 /* INCDEC_INDEX */, + 4 /* INCDEC_MEMBER */, + 4 /* CALL */, + 1 /* INDEX */, + 3 /* MEMBER */, + 1 /* AWAIT */, + 1 /* POP */, + 3 /* DECLARE */, + 2 /* DECLARE_LOCAL */, + 3 /* DECLARE_FAIL */, + 3 /* JUMP */, + 3 /* JUMP_IF_FALSE */, + 3 /* JUMP_IF_TRUE */, + 4 /* COMPARE_JUMP */, + 6 /* COMPARE_INT_JUMP */, + 7 /* COMPARE_LOCAL_INT_JUMP */, + 3 /* JUMP_IF_EQUAL */, + 1 /* FOREACH_BEGIN */, + 4 /* FOREACH_NEXT */, + 5 /* FUNC_DECL */, + 1 /* RETURN */, + 1 /* END */, + 4 /* CALL_NATIVE */, + 2 /* FREEZE */, + 5 /* ASSIGN_LOCAL_INT */, + 5 /* BINARY_LOCAL_INT */, + 4 /* BINARY_LOCAL_LOCAL */, + 6 /* COMPARE_LOCAL_LOCAL_JUMP */, + 2 /* INTRINSIC */, + 6 /* SET_LOCAL_BINARY_INT */, + 5 /* SET_LOCAL_BINARY_LOCAL */, + 3 /* SET_LOCAL_LOCAL */, + 3 /* BINARY_TOP_LOCAL */, + 4 /* MEMBER_LOCAL */, + 4 /* INDEX_LOCAL_INT */, + 3 /* INDEX_LOCAL_LOCAL */, + 3 /* STRUCT_ADD */ +}; diff --git a/src/dusk/script/core/bytecode/scriptbytecodesize.h b/src/dusk/script/core/bytecode/scriptbytecodesize.h new file mode 100644 index 00000000..38e15f1a --- /dev/null +++ b/src/dusk/script/core/bytecode/scriptbytecodesize.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BYTECODE_SCRIPTBYTECODESIZE_H +#define SCRIPT_BYTECODE_SCRIPTBYTECODESIZE_H + +#include "core/scriptcore.h" +#include "bytecode/scriptbytecode.h" + +/** + * How many bytes each instruction takes, its operands included, indexed by the + * instruction. + */ +extern const uint8_t scriptBytecodeSize[SCRIPT_BC_COUNT]; + +#endif diff --git a/src/dusk/script/core/bytecode/scriptvarint.c b/src/dusk/script/core/bytecode/scriptvarint.c new file mode 100644 index 00000000..889222ec --- /dev/null +++ b/src/dusk/script/core/bytecode/scriptvarint.c @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "core/scriptcore.h" +#include "bytecode/scriptvarint.h" + +uint8_t scriptVarintWrite(uint8_t *out, const uint16_t value) { + uint16_t rest = value; + uint8_t length = 0; + + while(rest >= 128) { + out[length++] = (uint8_t)((rest & 127) | 128); + rest = (uint16_t)(rest >> 7); + } + out[length++] = (uint8_t)rest; + return length; +} + +uint8_t scriptVarintRead(const uint8_t *in, uint16_t *value) { + uint16_t result = 0; + uint8_t length = 0; + uint8_t shift = 0; + uint8_t byte; + + do { + byte = in[length++]; + result = (uint16_t)(result | ((uint16_t)(byte & 127) << shift)); + shift += 7; + } while((byte & 128) && length < 3); + *value = result; + return length; +} + +uint16_t scriptZigzag(const int16_t value) { + return value < 0 ? (uint16_t)(((uint16_t)(-(value + 1)) << 1) | 1) : (uint16_t)((uint16_t)value << 1); +} + +int16_t scriptUnzigzag(const uint16_t value) { + return (value & 1) ? (int16_t)(-(int16_t)(value >> 1) - 1) : (int16_t)(value >> 1); +} diff --git a/src/dusk/script/core/bytecode/scriptvarint.h b/src/dusk/script/core/bytecode/scriptvarint.h new file mode 100644 index 00000000..3fc3040e --- /dev/null +++ b/src/dusk/script/core/bytecode/scriptvarint.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_BYTECODE_SCRIPTVARINT_H +#define SCRIPT_BYTECODE_SCRIPTVARINT_H + +#include "core/scriptcore.h" + +/** + * Writes a number in as few bytes as it needs, seven bits at a time with the + * top bit saying there is more. + * + * @param out Where to write, it needs 3 bytes of room. + * @param value The number. + * @returns How many bytes were written. + */ +uint8_t scriptVarintWrite(uint8_t *out, const uint16_t value); + +/** + * Reads a number written by scriptVarintWrite. + * + * @param in Where to read from. + * @param value Receives the number. + * @returns How many bytes were read. + */ +uint8_t scriptVarintRead(const uint8_t *in, uint16_t *value); + +/** + * Folds a signed number into an unsigned one so small numbers of either sign are + * small: 0, -1, 1, -2 become 0, 1, 2, 3. + * + * @param value The signed number. + * @returns The folded number. + */ +uint16_t scriptZigzag(const int16_t value); + +/** + * Undoes scriptZigzag. + * + * @param value The folded number. + * @returns The signed number. + */ +int16_t scriptUnzigzag(const uint16_t value); + +#endif diff --git a/src/dusk/script/core/call/scriptcall.c b/src/dusk/script/core/call/scriptcall.c new file mode 100644 index 00000000..67ceea80 --- /dev/null +++ b/src/dusk/script/core/call/scriptcall.c @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "call/scriptcall.h" +#include "promise/scriptpromisenew.h" +#include "coroutine/scriptstackcheck.h" +#include "error/scriptraise.h" +#include "value/scriptvaluecompare.h" +#include "value/scriptfunction.h" + +void scriptRunScriptFunction( + scriptruntime_t *runtime, + scriptfunction_t *function, + const scriptvalue_t *args, + const uint8_t argc, + scriptvalue_t *result +) { + if(!function->executor) scriptRaise(runtime, "%s has nothing to run it", function->name); + function->executor->run(runtime, function, args, argc, result); +} + +void scriptCallFunctionRef( + scriptruntime_t *runtime, + const scriptvalue_t *function, + const scriptvalue_t *args, + const uint8_t argc, + scriptvalue_t *result +) { + scriptfunction_t *fn; + scriptcoroutine_t *co = runtime->current; + scriptvalue_t returned; + scriptvalue_t promise; + const char *savedNative; + uint8_t i; + + scriptStackCheck(runtime); + if(function->type != SCRIPT_VALUE_FUNCTION) scriptRaise(runtime, "%s is not a function", scriptTypeName(*function)); + fn = function->as.function; + + if(fn->native) { + savedNative = co->nativeName; + co->nativeName = fn->name; + returned = fn->native(runtime, args, argc, fn); + co->nativeName = savedNative; + *result = returned; + return; + } + + if(fn->isAsync) { + promise = scriptPromiseNew(runtime); + promise.as.promise->hasTask = true; + promise.as.promise->taskFunction = *function; + promise.as.promise->taskArgc = argc; + for(i = 0; i < argc; i++) promise.as.promise->taskArgs[i] = args[i]; + *result = promise; + return; + } + + scriptRunScriptFunction(runtime, fn, args, argc, result); +} + +scriptvalue_t scriptCallFunction( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc +) { + scriptvalue_t result; + + scriptCallFunctionRef(runtime, &function, args, argc, &result); + return result; +} diff --git a/src/dusk/script/core/call/scriptcall.h b/src/dusk/script/core/call/scriptcall.h new file mode 100644 index 00000000..a4706116 --- /dev/null +++ b/src/dusk/script/core/call/scriptcall.h @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_CALL_SCRIPTCALL_H +#define SCRIPT_CALL_SCRIPTCALL_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" +#include "value/scriptfunction.h" + +/** + * Runs the body of a script function right now, in the current coroutine, with + * whichever executor made the function. + * + * @param runtime The runtime. + * @param function The function object. + * @param args The arguments. + * @param argc Number of arguments. + * @param result Receives what the function returned. + */ +void scriptRunScriptFunction( + scriptruntime_t *runtime, + scriptfunction_t *function, + const scriptvalue_t *args, + const uint8_t argc, + scriptvalue_t *result +); + +/** + * Calls a function value without copying the values around, which is what the + * virtual machine does. Everything else is as scriptCallFunction. The result may + * be where the function is. + * + * @param runtime The runtime. + * @param function The function to call, anything else is a fatal error. + * @param args The arguments. + * @param argc Number of arguments. + * @param result Receives what the function returned. + */ +void scriptCallFunctionRef( + scriptruntime_t *runtime, + const scriptvalue_t *function, + const scriptvalue_t *args, + const uint8_t argc, + scriptvalue_t *result +); + +/** + * Calls a function value. Script functions count towards the call depth, + * natives do not. Async functions give back a promise that starts when it is + * awaited. + * + * @param runtime The runtime. + * @param function The function to call, anything else is a fatal error. + * @param args The arguments. + * @param argc Number of arguments. + * @returns What the function returned. + */ +scriptvalue_t scriptCallFunction( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc +); + +#endif diff --git a/src/dusk/script/core/core/scriptcore.h b/src/dusk/script/core/core/scriptcore.h new file mode 100644 index 00000000..01743eda --- /dev/null +++ b/src/dusk/script/core/core/scriptcore.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_CORE_SCRIPTCORE_H +#define SCRIPT_CORE_SCRIPTCORE_H + +#include +#include +#include +#include "core/scriptplatform.h" + +// Limits, see script.md. Going over any of them is a fatal error. +#define SCRIPT_CALL_DEPTH_MAX 8 + +#define SCRIPT_STRUCT_KEYS_MAX 16 + +#define SCRIPT_STRING_LENGTH_MAX 512 + +#define SCRIPT_ARRAY_LENGTH_MAX 512 + +#define SCRIPT_PARAMS_MAX 8 + +// Sizes used by errors and stack traces. +#define SCRIPT_NAME_MAX 32 + +#define SCRIPT_FILE_MAX 96 + +#define SCRIPT_ERROR_MESSAGE_MAX 160 + +#define SCRIPT_ERROR_FRAMES_MAX 24 + +typedef enum { + SCRIPT_OK, + SCRIPT_ERROR_SYNTAX, + SCRIPT_ERROR_RUNTIME, + SCRIPT_ERROR_UNSUPPORTED +} scriptresult_t; + +typedef struct { + char name[SCRIPT_NAME_MAX]; + char file[SCRIPT_FILE_MAX]; + uint16_t line; + uint16_t column; +} scriptframeinfo_t; + +typedef struct { + scriptresult_t code; + char message[SCRIPT_ERROR_MESSAGE_MAX]; + uint8_t frameCount; + scriptframeinfo_t frames[SCRIPT_ERROR_FRAMES_MAX]; +} scripterror_t; + +#endif diff --git a/src/dusk/script/core/core/scriptplatform.h b/src/dusk/script/core/core/scriptplatform.h new file mode 100644 index 00000000..8d120aab --- /dev/null +++ b/src/dusk/script/core/core/scriptplatform.h @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_CORE_SCRIPTPLATFORM_H +#define SCRIPT_CORE_SCRIPTPLATFORM_H + +// What the platform the script engine is built for can do, and the sizes its +// numbers come in. Every one of the feature switches can be set from the +// compiler command line, so a feature of a small platform can be tried on a big +// one. + +#include +#include +#include +#include "util/memory.h" + +#ifdef __65816__ + // The Super Nintendo, built with PVSnesLib's 816-tcc. An int is 16 bits and a + // long long is 32, there is nothing bigger, no ucontext, no maths library and no + // #pragma once. + #define SCRIPT_PLATFORM_SNES 1 + + #ifndef SCRIPT_FEATURE_COROUTINES + #define SCRIPT_FEATURE_COROUTINES 0 + #endif + #ifndef SCRIPT_FEATURE_MATH + #define SCRIPT_FEATURE_MATH 0 + #endif + // There is nowhere to print to on the Super Nintendo, so print is not a function + // there, a script that calls it is calling something that is not defined. The test + // ROM puts it back, to log what the test scripts print. + #ifndef SCRIPT_FEATURE_PRINT + #define SCRIPT_FEATURE_PRINT 0 + #endif + // The compiler has checked what it could of the arguments of the natives that have a + // signature (natives.json), so the machine only checks the rest and the natives take what they + // are given as what it should be. A platform that has the room does all of it as it runs. + #ifndef SCRIPT_FEATURE_TRUST_BYTECODE + #define SCRIPT_FEATURE_TRUST_BYTECODE 1 + #endif + + // The widest numbers there are. A bignum on this platform only has 32 bits. + typedef int32_t scriptwide_t; + typedef uint32_t scriptuwide_t; + + // Pointers are 24 bits and turning one into a number loses the bank, so + // nothing here does that: places in the heap are offsets from its start, and + // the collector looks for pointers every 2 bytes. + #define SCRIPT_SCAN_STEP 2 + + #define SCRIPT_STATIC_ASSERT(condition, message) + + // Every block of the heap has a header of 8 bytes and starts on 4. A size is 16 bits, + // and the check that a block is a block, which the collector uses to tell a stray + // number from a block, is 16 bits as well. + #define SCRIPT_HEAP_ALIGN 4 + + // The collector does not run before the heap is this much of full: a script that is + // small never needs it, and a run of it takes about a tenth of a second here. + #define SCRIPT_GC_MINIMUM(heapSize) ((heapSize) / 2) + typedef uint16_t scriptheapsize_t; + #define SCRIPT_HEAP_PADDING 0 + #define SCRIPT_HEAP_CHECK 0x3CC3u + + // A fill written in assembly, which is quicker than the library's, it is in + // src/dusksnes/script/scriptmem.asm. + void *scriptMemFill(void *destination, int value, size_t length); + #define SCRIPT_MEMSET(destination, value, length) scriptMemFill(destination, value, length) + + // The smallest block of memory a runtime can be given, how many things the + // collector can have waiting at once, how many timers can be waiting, and the + // most source text that is read from a file. + #define SCRIPT_RUNTIME_MIN 16384 + #define SCRIPT_GC_STACK_MAX 128 + #define SCRIPT_TIMERS_MAX 4 + #define SCRIPT_SOURCE_MAX 2048 + + // How many values the variables and stacks of the functions running at once + // can take together, in each coroutine. + #define SCRIPT_VM_VALUES_MAX 160 + + // Fills a variable up to 16 bytes, see scriptvariable_t. + #define SCRIPT_VARIABLE_PADDING 4 + #define SCRIPT_FRAME_PADDING 12 + + // The global variables, a compiled script finds each one once when it loads + // and keeps a pointer to it, so they have room for all of them to start with. + #define SCRIPT_GLOBALS_MAX 48 + + // Room for the functions and the constants the game adds to the built in ones + #define SCRIPT_BUILTINS_SPARE 32 + + // The compiler keeps values in the stack, there are no registers to flush. + #define SCRIPT_UNWIND_INIT() + + // How much of the stack a script may use before it is called too deep, the + // stack is 8 KB and the rest of the program needs some. + #define SCRIPT_STACK_BUDGET 4096 +#else + #define SCRIPT_PLATFORM_SNES 0 + + #ifndef SCRIPT_FEATURE_COROUTINES + #define SCRIPT_FEATURE_COROUTINES 1 + #endif + #ifndef SCRIPT_FEATURE_MATH + #define SCRIPT_FEATURE_MATH 1 + #endif + #ifndef SCRIPT_FEATURE_PRINT + #define SCRIPT_FEATURE_PRINT 1 + #endif + #ifndef SCRIPT_FEATURE_TRUST_BYTECODE + #define SCRIPT_FEATURE_TRUST_BYTECODE 0 + #endif + + typedef int64_t scriptwide_t; + typedef uint64_t scriptuwide_t; + + // The collector looks for pointers where a pointer can be. + #define SCRIPT_SCAN_STEP sizeof(void *) + + #define SCRIPT_STATIC_ASSERT(condition, message) _Static_assert(condition, message) + #define SCRIPT_MEMSET(destination, value, length) memorySet(destination, value, length) + + #define SCRIPT_RUNTIME_MIN 262144 + #define SCRIPT_GC_STACK_MAX 1024 + #define SCRIPT_TIMERS_MAX 32 + #define SCRIPT_SOURCE_MAX 32768 + #define SCRIPT_VM_VALUES_MAX 1536 + + // Fills a variable up to 32 bytes, see scriptvariable_t. + #define SCRIPT_VARIABLE_PADDING 8 + #define SCRIPT_FRAME_PADDING 8 + + #define SCRIPT_GLOBALS_MAX 128 + #define SCRIPT_BUILTINS_SPARE 64 + + // Blocks of the heap have a header of 32 bytes and start on 16. + #define SCRIPT_HEAP_ALIGN 16 + #define SCRIPT_GC_MINIMUM(heapSize) ((heapSize) / 8) + typedef uint32_t scriptheapsize_t; + #define SCRIPT_HEAP_PADDING 14 + #define SCRIPT_HEAP_CHECK 0x5A5AA5A5C3C33C3CULL + + // Makes the compiler put every register it is keeping a value in on the + // stack, so a search of the stack sees them. + #define SCRIPT_UNWIND_INIT() __builtin_unwind_init() + + // The stack of the script when it can not have a stack of its own. + #define SCRIPT_STACK_BUDGET 65536 +#endif + +// A place in the heap, counted in bytes from the start of it. +typedef size_t scriptoffset_t; + +// Most values a function has on its stack at once, the compiler checks it. +#define SCRIPT_VM_STACK_MAX 64 + +#endif diff --git a/src/dusk/script/core/core/scriptsource.h b/src/dusk/script/core/core/scriptsource.h new file mode 100644 index 00000000..3dcc3fec --- /dev/null +++ b/src/dusk/script/core/core/scriptsource.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_CORE_SCRIPTSOURCE_H +#define SCRIPT_CORE_SCRIPTSOURCE_H + +#include "core/scriptcore.h" + +// A script's text and the name shown for it in errors. +typedef struct scriptsource_t { + char name[SCRIPT_FILE_MAX]; + char path[SCRIPT_FILE_MAX]; + const char *text; + size_t length; +} scriptsource_t; + +struct scriptsource_t; + +#endif diff --git a/src/dusk/script/core/coroutine/scriptcoroutine.h b/src/dusk/script/core/coroutine/scriptcoroutine.h new file mode 100644 index 00000000..eb900f2a --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptcoroutine.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTCOROUTINE_H +#define SCRIPT_COROUTINE_SCRIPTCOROUTINE_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" +#include "coroutine/scriptframe.h" +#include +#if SCRIPT_FEATURE_COROUTINES +#include +#endif + +#define SCRIPT_STACK_SIZE 65536 + +typedef enum { + SCRIPT_COROUTINE_READY, + SCRIPT_COROUTINE_RUNNING, + SCRIPT_COROUTINE_WAITING, + SCRIPT_COROUTINE_DONE, + SCRIPT_COROUTINE_FAILED +} scriptcostate_t; + +// A script that can be paused at an await. Where there is no way to switch stacks +// (SCRIPT_FEATURE_COROUTINES is 0) it runs on the stack of whoever starts it, to +// the end, and an await can only wait for what can be run right then. +typedef struct scriptcoroutine_t { + struct scriptcoroutine_t *nextAll; + struct scriptcoroutine_t *prevAll; +#if SCRIPT_FEATURE_COROUTINES + ucontext_t context; + void *stack; +#endif + jmp_buf errorJump; + scriptcostate_t state; + struct scriptcoroutine_t *nextReady; + scriptvalue_t function; + scriptvalue_t args[SCRIPT_PARAMS_MAX]; + uint8_t argc; + scriptpromise_t *promise; + scriptframe_t frames[SCRIPT_CALL_DEPTH_MAX + 1]; + uint8_t depth; // number of frames in use, frames[depth - 1] is the current one + const char *nativeName; + + // The variables and the stack of the functions the virtual machine is running + // for this coroutine, one after the other. The block is made when the first + // function starts. + scriptvalue_t *values; + uint16_t valuesTop; +} scriptcoroutine_t; + +struct scriptcoroutine_t; + +#endif diff --git a/src/dusk/script/core/coroutine/scriptcoroutineready.c b/src/dusk/script/core/coroutine/scriptcoroutineready.c new file mode 100644 index 00000000..a7198c1a --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptcoroutineready.c @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptcoroutineready.h" + +void scriptReadyPush(scriptruntime_t *runtime, scriptcoroutine_t *coroutine) { + coroutine->state = SCRIPT_COROUTINE_READY; + coroutine->nextReady = NULL; + if(runtime->readyTail) runtime->readyTail->nextReady = coroutine; + else runtime->readyHead = coroutine; + runtime->readyTail = coroutine; +} diff --git a/src/dusk/script/core/coroutine/scriptcoroutineready.h b/src/dusk/script/core/coroutine/scriptcoroutineready.h new file mode 100644 index 00000000..86635a4d --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptcoroutineready.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTCOROUTINEREADY_H +#define SCRIPT_COROUTINE_SCRIPTCOROUTINEREADY_H + +#include "core/scriptcore.h" +#include "coroutine/scriptcoroutine.h" + +/** + * Puts a coroutine in the ready queue. + * + * @param runtime The runtime. + * @param coroutine The coroutine, it is marked ready. + */ +void scriptReadyPush(scriptruntime_t *runtime, scriptcoroutine_t *coroutine); + +#endif diff --git a/src/dusk/script/core/coroutine/scriptcoroutinespawn.c b/src/dusk/script/core/coroutine/scriptcoroutinespawn.c new file mode 100644 index 00000000..d6bc3cef --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptcoroutinespawn.c @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptcoroutinespawn.h" +#include "value/scriptalloc.h" +#include "promise/scriptpromisesettle.h" +#include "coroutine/scriptcoroutineready.h" +#include "call/scriptcall.h" +#include "value/scriptvalueprimitive.h" +#include "value/scriptfunction.h" +#include "scope/scriptscopeset.h" +#include "runtime/scriptruntimerun.h" +#include +#if SCRIPT_FEATURE_COROUTINES +#include +#endif + +#if SCRIPT_FEATURE_COROUTINES +// makecontext can not pass a pointer on every platform, so the runtime a new +// coroutine belongs to is handed over through here just before it starts. +static scriptruntime_t *scriptEntryRuntime = NULL; +#endif + +void scriptCoroutineRun(scriptruntime_t *runtime, scriptcoroutine_t *co) { + if(setjmp(co->errorJump) == 0) { + scriptvalue_t result; + scriptRunScriptFunction(runtime, co->function.as.function, co->args, co->argc, &result); + co->state = SCRIPT_COROUTINE_DONE; + // What the main script returns is kept in the global scope, where the collector sees it + if(co == runtime->root && runtime->captureResult) scriptScopeDefine(runtime, runtime->globalScope, SCRIPT_RESULT_NAME, result); + if(co->promise) scriptPromiseResolve(runtime, co->promise, result); + } else { + co->state = SCRIPT_COROUTINE_FAILED; + } +} + +#if SCRIPT_FEATURE_COROUTINES +void scriptCoroutineEntry(void) { + scriptruntime_t *runtime = scriptEntryRuntime; + scriptCoroutineRun(runtime, runtime->current); +} +#endif + +scriptcoroutine_t *scriptCoroutineNew( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc, + scriptpromise_t *promise +) { + scriptcoroutine_t *co = scriptAlloc(runtime, sizeof(scriptcoroutine_t)); + uint8_t i; + + co->function = function; + co->argc = argc; + for(i = 0; i < argc; i++) co->args[i] = args[i]; + co->promise = promise; +#if SCRIPT_FEATURE_COROUTINES + co->stack = scriptAlloc(runtime, SCRIPT_STACK_SIZE); + + getcontext(&co->context); + co->context.uc_stack.ss_sp = co->stack; + co->context.uc_stack.ss_size = SCRIPT_STACK_SIZE; + co->context.uc_link = &runtime->schedulerContext; + makecontext(&co->context, (void (*)(void))scriptCoroutineEntry, 0); +#endif + + co->nextAll = runtime->coroutines; + if(runtime->coroutines) runtime->coroutines->prevAll = co; + runtime->coroutines = co; + + runtime->liveCoroutines++; + return co; +} + +scriptcoroutine_t *scriptCoroutineSpawn( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc, + scriptpromise_t *promise +) { + scriptcoroutine_t *co = scriptCoroutineNew(runtime, function, args, argc, promise); + + scriptReadyPush(runtime, co); + return co; +} + +void scriptCoroutineResume(scriptruntime_t *runtime, scriptcoroutine_t *coroutine) { + runtime->current = coroutine; + coroutine->state = SCRIPT_COROUTINE_RUNNING; + runtime->abortTarget = &coroutine->errorJump; +#if SCRIPT_FEATURE_COROUTINES + scriptEntryRuntime = runtime; + swapcontext(&runtime->schedulerContext, &coroutine->context); +#else + scriptCoroutineRun(runtime, coroutine); +#endif + runtime->current = NULL; + runtime->abortTarget = &runtime->schedulerJump; +} diff --git a/src/dusk/script/core/coroutine/scriptcoroutinespawn.h b/src/dusk/script/core/coroutine/scriptcoroutinespawn.h new file mode 100644 index 00000000..ce64d27b --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptcoroutinespawn.h @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTCOROUTINESPAWN_H +#define SCRIPT_COROUTINE_SCRIPTCOROUTINESPAWN_H + +#include "core/scriptcore.h" +#include "coroutine/scriptcoroutine.h" +#include "value/scriptvalue.h" + +/** + * Runs the function of a coroutine to the end and records how it ended, a fatal + * error ends it as failed. + * + * @param runtime The runtime. + * @param coroutine The coroutine, it is the current one. + */ +void scriptCoroutineRun(scriptruntime_t *runtime, scriptcoroutine_t *coroutine); + +#if SCRIPT_FEATURE_COROUTINES +/** + * Where a new coroutine starts, it has a stack of its own to run on. + */ +void scriptCoroutineEntry(void); +#endif + +/** + * Makes a coroutine, which is in the list the collector looks through and is not ready to run: whoever made it runs it. + * + * @param runtime The runtime. + * @param function The function it runs. + * @param args The arguments. + * @param argc How many there are. + * @param promise What is settled with what it returns, NULL for none. + * @returns The coroutine. + */ +scriptcoroutine_t *scriptCoroutineNew( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc, + scriptpromise_t *promise +); + +/** + * Makes the runtime start a coroutine for a function. + * + * @param runtime The runtime. + * @param function The function value to run. + * @param args The arguments. + * @param argc Number of arguments. + * @param promise Promise settled with the result, may be NULL. + * @returns The coroutine, it is waiting in the ready queue. + */ +scriptcoroutine_t *scriptCoroutineSpawn( + scriptruntime_t *runtime, + const scriptvalue_t function, + const scriptvalue_t *args, + const uint8_t argc, + scriptpromise_t *promise +); + +/** + * Runs a coroutine until it waits, finishes or fails. + * + * @param runtime The runtime. + * @param coroutine The coroutine to switch to. + */ +void scriptCoroutineResume(scriptruntime_t *runtime, scriptcoroutine_t *coroutine); + +#endif diff --git a/src/dusk/script/core/coroutine/scriptframe.h b/src/dusk/script/core/coroutine/scriptframe.h new file mode 100644 index 00000000..7bfba3de --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptframe.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTFRAME_H +#define SCRIPT_COROUTINE_SCRIPTFRAME_H + +#include "core/scriptcore.h" +#include "value/scriptvalue.h" +#include "core/scriptsource.h" + +// The virtual machine does not keep the position up to date while it runs, it +// works it out from where the frame is in the code when something wants to know, +// which is when there is an error. +// +// The name and the script of a frame are the ones of its function, they are not +// copied in for every call. The line and column are kept up to date by the +// interpreter, the machine leaves them and has the executor of the function +// work them out from locateData. +typedef struct { + const scriptfunction_t *function; + uint16_t line; + uint16_t column; + scriptvalue_t returnValue; + void *locateData; + uint8_t padding[SCRIPT_FRAME_PADDING]; // sizeof is a power of two on the SNES, the frames are indexed with a shift +} scriptframe_t; + +#endif diff --git a/src/dusk/script/core/coroutine/scriptframelocate.c b/src/dusk/script/core/coroutine/scriptframelocate.c new file mode 100644 index 00000000..3904d01a --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptframelocate.c @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptframelocate.h" + +void scriptFrameLocate(const scriptframe_t *frame, uint16_t *line, uint16_t *column) { + const scriptexecutor_t *executor = frame->function->executor; + *line = frame->line; + *column = frame->column; + if(executor && executor->locate && frame->locateData) executor->locate(frame->function, frame->locateData, line, column); +} + +const char *scriptFrameName(const scriptframe_t *frame) { + return frame->function->name[0] ? frame->function->name : ""; +} + +const char *scriptFrameFile(const scriptframe_t *frame) { + return frame->function->source ? frame->function->source->name : ""; +} diff --git a/src/dusk/script/core/coroutine/scriptframelocate.h b/src/dusk/script/core/coroutine/scriptframelocate.h new file mode 100644 index 00000000..685453f6 --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptframelocate.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +#include "scripttypes.h" + +/** + * Finds where a frame is in its script. + * + * @param frame The frame. + * @param line Receives the line. + * @param column Receives the column. + */ +void scriptFrameLocate(const scriptframe_t *frame, uint16_t *line, uint16_t *column); + +/** + * @param frame The frame. + * @returns The name of the function of the frame, for a stack trace. + */ +const char *scriptFrameName(const scriptframe_t *frame); + +/** + * @param frame The frame. + * @returns The name of the script of the frame, empty when it has none. + */ +const char *scriptFrameFile(const scriptframe_t *frame); diff --git a/src/dusk/script/core/coroutine/scriptscheduler.c b/src/dusk/script/core/coroutine/scriptscheduler.c new file mode 100644 index 00000000..1adc8ab0 --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptscheduler.c @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptscheduler.h" +#include "coroutine/scriptcoroutinespawn.h" +#include "value/scriptalloc.h" +#include "error/scriptraise.h" +#include "promise/scripttimer.h" +#include + +scriptresult_t scriptSchedulerRun(scriptruntime_t *runtime) { + scriptcoroutine_t *co; + + runtime->abortTarget = &runtime->schedulerJump; + if(setjmp(runtime->schedulerJump) != 0) { + runtime->abortTarget = NULL; + return runtime->error.code; + } + + while(true) { + co = runtime->readyHead; + if(co) { + runtime->readyHead = co->nextReady; + if(!runtime->readyHead) runtime->readyTail = NULL; + + scriptCoroutineResume(runtime, co); + + if(co->state == SCRIPT_COROUTINE_FAILED) { + runtime->abortTarget = NULL; + return runtime->error.code; + } + if(co->state == SCRIPT_COROUTINE_DONE) scriptCoroutineReap(runtime, co); + continue; + } + + if(runtime->liveCoroutines == 0) break; + + if(runtime->timerCount > 0) { + scriptTimersAdvance(runtime); + continue; + } + + // Everything left is waiting on something that can never happen + runtime->current = runtime->root; + scriptRaise(runtime, "The script is waiting for a promise that never settles"); + } + + runtime->abortTarget = NULL; + return SCRIPT_OK; +} + +void scriptCoroutineReap(scriptruntime_t *runtime, scriptcoroutine_t *co) { + if(co == runtime->root) runtime->root = NULL; + if(co->prevAll) co->prevAll->nextAll = co->nextAll; + else runtime->coroutines = co->nextAll; + if(co->nextAll) co->nextAll->prevAll = co->prevAll; +#if SCRIPT_FEATURE_COROUTINES + scriptFree(runtime, co->stack); +#endif + if(co->values) scriptFree(runtime, co->values); + scriptFree(runtime, co); + runtime->liveCoroutines--; +} + +#if !SCRIPT_FEATURE_COROUTINES +bool scriptSchedulerStep(scriptruntime_t *runtime) { + scriptcoroutine_t *co = runtime->readyHead; + scriptcoroutine_t *outer = runtime->current; + jmp_buf *outerAbort = runtime->abortTarget; + + if(co) { + runtime->readyHead = co->nextReady; + if(!runtime->readyHead) runtime->readyTail = NULL; + + scriptCoroutineResume(runtime, co); + runtime->current = outer; + runtime->abortTarget = outerAbort; + + // A coroutine that failed has already said why, so the one waiting for it + // fails too without saying anything more + if(co->state == SCRIPT_COROUTINE_FAILED) longjmp(*outerAbort, 1); + if(co->state == SCRIPT_COROUTINE_DONE) scriptCoroutineReap(runtime, co); + return true; + } + + if(runtime->timerCount > 0) { + scriptTimersAdvance(runtime); + return true; + } + return false; +} +#endif diff --git a/src/dusk/script/core/coroutine/scriptscheduler.h b/src/dusk/script/core/coroutine/scriptscheduler.h new file mode 100644 index 00000000..aa5c8b5e --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptscheduler.h @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTSCHEDULER_H +#define SCRIPT_COROUTINE_SCRIPTSCHEDULER_H + +#include "core/scriptcore.h" + +/** + * Runs everything that is ready, and keeps going until no coroutine is left or + * something can never finish. A fatal error ends it early. + * + * @param runtime The runtime. + * @returns SCRIPT_OK, or the kind of error with the details in runtime->error. + */ +scriptresult_t scriptSchedulerRun(scriptruntime_t *runtime); + +/** + * Takes a finished coroutine out of the runtime and frees it. + * + * @param runtime The runtime. + * @param co The coroutine, it has to be done. + */ +void scriptCoroutineReap(scriptruntime_t *runtime, scriptcoroutine_t *co); + +#if !SCRIPT_FEATURE_COROUTINES +/** + * Runs whatever is next, a coroutine that is ready or the timer that is due + * first, from inside another coroutine. This is how an await makes progress + * when a coroutine can not be paused. A coroutine that fails fails whoever + * called this too. + * + * @param runtime The runtime. + * @returns False when there is nothing left that could run. + */ +bool scriptSchedulerStep(scriptruntime_t *runtime); +#endif + +#endif diff --git a/src/dusk/script/core/coroutine/scriptsetposition.c b/src/dusk/script/core/coroutine/scriptsetposition.c new file mode 100644 index 00000000..f1beaedb --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptsetposition.c @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptsetposition.h" + +void scriptSetPosition(scriptruntime_t *runtime, const uint16_t line, const uint16_t column) { + scriptcoroutine_t *co = runtime->current; + if(co && co->depth > 0) { + co->frames[co->depth - 1].line = line; + co->frames[co->depth - 1].column = column; + } +} diff --git a/src/dusk/script/core/coroutine/scriptsetposition.h b/src/dusk/script/core/coroutine/scriptsetposition.h new file mode 100644 index 00000000..ca895acc --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptsetposition.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTSETPOSITION_H +#define SCRIPT_COROUTINE_SCRIPTSETPOSITION_H + +#include "core/scriptcore.h" + +/** + * Moves the current frame's position to a node, so errors report it. + * + * @param runtime The runtime. + * @param line Line in the script. + * @param column Column in the script. + */ +void scriptSetPosition(scriptruntime_t *runtime, const uint16_t line, const uint16_t column); + +#endif diff --git a/src/dusk/script/core/coroutine/scriptstackcheck.c b/src/dusk/script/core/coroutine/scriptstackcheck.c new file mode 100644 index 00000000..49d2ea59 --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptstackcheck.c @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "coroutine/scriptstackcheck.h" +#include "error/scriptraise.h" + +void scriptStackCheck(scriptruntime_t *runtime) { + char marker; +#if SCRIPT_FEATURE_COROUTINES + if(runtime->current && (size_t)(&marker - (char *)runtime->current->stack) < SCRIPT_STACK_RESERVE) { + scriptRaise(runtime, "The script is nested too deeply, the stack is full"); + } +#else + // The stack grows down from where the script was started + if(runtime->mainStackBase && (size_t)(runtime->mainStackBase - &marker) > SCRIPT_STACK_BUDGET) { + scriptRaise(runtime, "The script is nested too deeply, the stack is full"); + } +#endif +} diff --git a/src/dusk/script/core/coroutine/scriptstackcheck.h b/src/dusk/script/core/coroutine/scriptstackcheck.h new file mode 100644 index 00000000..a58ea734 --- /dev/null +++ b/src/dusk/script/core/coroutine/scriptstackcheck.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_COROUTINE_SCRIPTSTACKCHECK_H +#define SCRIPT_COROUTINE_SCRIPTSTACKCHECK_H + +#include "core/scriptcore.h" + +#define SCRIPT_STACK_RESERVE 8192 + +/** + * Raises a fatal error when the current coroutine's stack is nearly used up, + * which is what a script that nests expressions or calls far too deeply would + * otherwise do by crashing. + * + * @param runtime The runtime. + */ +void scriptStackCheck(scriptruntime_t *runtime); + +#endif diff --git a/src/dusk/script/core/duskscript.h b/src/dusk/script/core/duskscript.h new file mode 100644 index 00000000..ac95b426 --- /dev/null +++ b/src/dusk/script/core/duskscript.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_DUSKSCRIPT_H +#define SCRIPT_DUSKSCRIPT_H + +// The part of dusk script every runtime shares: values, the heap, the garbage +// collector, the built in functions and running scripts. The interpreter and the +// virtual machine both build on it. + +#include "core/scriptcore.h" +#include "runtime/scriptruntime.h" +#include "runtime/scriptruntimecreate.h" +#include "runtime/scriptruntimeconfig.h" +#include "runtime/scriptruntimerun.h" +#include "error/scripttrace.h" + +#endif diff --git a/src/dusk/script/core/duskscriptnative.h b/src/dusk/script/core/duskscriptnative.h new file mode 100644 index 00000000..130cd48b --- /dev/null +++ b/src/dusk/script/core/duskscriptnative.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_DUSKSCRIPTNATIVE_H +#define SCRIPT_DUSKSCRIPTNATIVE_H + +// What a native function needs, the types, the argument helpers and the value +// constructors. + +#include "duskscript.h" +#include "scripttypes.h" +#include "builtin/scriptbuiltin.h" +#include "builtin/scriptarg.h" +#include "builtin/scriptbuiltinregister.h" +#include "error/scriptraise.h" +#include "value/scriptvalueprimitive.h" +#include "string/scriptstringnew.h" +#include "array/scriptarraynew.h" +#include "array/scriptarraymodify.h" +#include "struct/scriptstruct.h" +#include "promise/scriptpromisenew.h" +#include "promise/scriptpromisesettle.h" +#include "promise/scripttimer.h" +#include "runtime/scriptruntimeload.h" + +#endif diff --git a/src/dusk/script/core/error/scriptraise.c b/src/dusk/script/core/error/scriptraise.c new file mode 100644 index 00000000..bfad3909 --- /dev/null +++ b/src/dusk/script/core/error/scriptraise.c @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "error/scriptraise.h" +#include "text/scripttext.h" +#include "coroutine/scriptframelocate.h" +#include +#include +#include +#include + +void scriptRaise(scriptruntime_t *runtime, const char *format, ...) { + va_list args; + scripterror_t *error = &runtime->error; + scriptcoroutine_t *co = runtime->current; + int i; + + SCRIPT_MEMSET(error, 0, sizeof(scripterror_t)); + error->code = SCRIPT_ERROR_RUNTIME; + va_start(args, format); + scriptFormatV(error->message, sizeof(error->message), format, args); + va_end(args); + + if(co) { + // A native that is running shows up as its own frame, at the line of the + // script that called it. + if(co->nativeName && co->depth > 0) { + scriptframeinfo_t *info = &error->frames[error->frameCount++]; + scriptCopyText(info->name, sizeof(info->name), co->nativeName); + scriptCopyText(info->file, sizeof(info->file), scriptFrameFile(&co->frames[co->depth - 1])); + scriptFrameLocate(&co->frames[co->depth - 1], &info->line, &info->column); + } + + for(i = (int)co->depth - 1; i >= 0 && error->frameCount < SCRIPT_ERROR_FRAMES_MAX; i--) { + scriptframeinfo_t *info = &error->frames[error->frameCount++]; + scriptCopyText(info->name, sizeof(info->name), scriptFrameName(&co->frames[i])); + scriptCopyText(info->file, sizeof(info->file), scriptFrameFile(&co->frames[i])); + scriptFrameLocate(&co->frames[i], &info->line, &info->column); + } + } + + if(!runtime->abortTarget) abort(); + longjmp(*runtime->abortTarget, 1); +} + +void scriptRaiseSyntax( + scriptruntime_t *runtime, + const scriptsource_t *source, + const uint16_t line, + const uint16_t column, + const char *format, + ... +) { + va_list args; + scripterror_t *error = &runtime->error; + scriptframeinfo_t *info; + + SCRIPT_MEMSET(error, 0, sizeof(scripterror_t)); + error->code = SCRIPT_ERROR_SYNTAX; + va_start(args, format); + scriptFormatV(error->message, sizeof(error->message), format, args); + va_end(args); + + info = &error->frames[error->frameCount++]; + scriptCopyText(info->name, sizeof(info->name), ""); + scriptCopyText(info->file, sizeof(info->file), source ? source->name : ""); + info->line = line; + info->column = column; + + if(!runtime->abortTarget) abort(); + longjmp(*runtime->abortTarget, 1); +} diff --git a/src/dusk/script/core/error/scriptraise.h b/src/dusk/script/core/error/scriptraise.h new file mode 100644 index 00000000..4c61399f --- /dev/null +++ b/src/dusk/script/core/error/scriptraise.h @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ERROR_SCRIPTRAISE_H +#define SCRIPT_ERROR_SCRIPTRAISE_H + +#include "core/scriptcore.h" +#include "core/scriptsource.h" + +/** + * Raises a fatal runtime error at the position the script is up to. It builds + * the stack trace and jumps out of whatever was running, it does not return. + * + * @param runtime The runtime. + * @param format Message format, see scriptFormatV. + * @param ... What the format takes. + */ +void scriptRaise(scriptruntime_t *runtime, const char *format, ...); + +/** + * Raises a fatal syntax error at a position in a script, it does not return. + * + * @param runtime The runtime. + * @param source The script. + * @param line Line of the error, starting at 1. + * @param column Column of the error, starting at 1. + * @param format Message format, see scriptFormatV. + * @param ... What the format takes. + */ +void scriptRaiseSyntax( + scriptruntime_t *runtime, + const scriptsource_t *source, + const uint16_t line, + const uint16_t column, + const char *format, + ... +); + +#endif diff --git a/src/dusk/script/core/error/scripttrace.c b/src/dusk/script/core/error/scripttrace.c new file mode 100644 index 00000000..3a01e7d0 --- /dev/null +++ b/src/dusk/script/core/error/scripttrace.c @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scripttypes.h" +#include "error/scripttrace.h" +#include "text/scripttext.h" +#include + +void scriptErrorFormat(const scripterror_t *error, char *buffer, const size_t size) { + size_t length = 0; + size_t remaining; + size_t lineLength; + uint8_t i; + const char *suffix; + + if(size == 0) return; + buffer[0] = 0; + + // Each line is formatted where it goes, and taken back when it does not fit + // whole. The text of a line can be long, so there is no room for a copy of it. + remaining = size; + // A frame has no line when the script has no map, and then there is nothing to say of where it was + if(error->frameCount > 0 && error->frames[0].line > 0) { + suffix = error->code == SCRIPT_ERROR_SYNTAX ? "" : ":"; + scriptFormat( + buffer, + remaining, + "Error [%s] in %s (%u:%u)%s\n", + error->message, + error->frames[0].file, + (unsigned int)error->frames[0].line, + (unsigned int)error->frames[0].column, + suffix + ); + } else { + scriptFormat(buffer, remaining, "Error [%s]\n", error->message); + } + lineLength = strlen(buffer); + if(lineLength + 1 >= remaining) { + buffer[0] = 0; + return; + } + length = lineLength; + + if(error->code == SCRIPT_ERROR_SYNTAX) return; + + for(i = 0; i < error->frameCount; i++) { + remaining = size - length; + if(error->frames[i].line > 0) { + scriptFormat( + buffer + length, + remaining, + " at %s (%s:%u:%u)\n", + error->frames[i].name, + error->frames[i].file, + (unsigned int)error->frames[i].line, + (unsigned int)error->frames[i].column + ); + } else { + scriptFormat(buffer + length, remaining, " at %s\n", error->frames[i].name); + } + lineLength = strlen(buffer + length); + if(lineLength + 1 >= remaining) { + buffer[length] = 0; + break; + } + length += lineLength; + } +} diff --git a/src/dusk/script/core/error/scripttrace.h b/src/dusk/script/core/error/scripttrace.h new file mode 100644 index 00000000..9cf9a28e --- /dev/null +++ b/src/dusk/script/core/error/scripttrace.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#ifndef SCRIPT_ERROR_SCRIPTTRACE_H +#define SCRIPT_ERROR_SCRIPTTRACE_H + +#include "core/scriptcore.h" + +/** + * Writes an error as a stack trace, for example + * + * Error [Division by zero] in test.ds (14:10): + * at divide (test.ds:14:10) + * at