diff --git a/CLAUDE.md b/CLAUDE.md index 201d0dd1a3..575b6e7f98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ Task-specific instructions are split into skill files under `skills/`. You MUST | `skills/internal/documentation_rst.md` | Editing RST in `doc/source/`, `//!` doc-comments in `daslib/*.das`, tutorial RST pages | | `skills/internal/tutorials.md` | Anything that looks like a tutorial - they live under `/tutorials//`, NEVER `modules//tutorial/` | | `skills/internal/tutorial_prose.md` | WRITING or revising general-reader doc/tutorial prose (`documentation_rst.md` is mechanics, this is the words) | -| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums | +| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`) | | `skills/internal/cpp_codebase_notes.md` | Working on daslang's own C++ - where inference/builtins/errors/parser live, AST function flags | | `skills/internal/clang_bind_build.md` | Enabling `dasClangBind` / bumping the libclang SDK / running any `bind_*.das` self-binder | | `skills/daslib_modules.md` | Working with `daslib/` modules or extending the stdlib | @@ -258,6 +258,7 @@ Most layout is obvious from `ls`. The non-obvious ones: - `skills/daslang/` - the **distributable, SDK-free daslang language skill** for third-party AI agents (`SKILL.md` + `references/`), NOT a repo task skill. Every example is probe-verified; its `README.md` carries the editing rules. Grammar/stdlib/default changes update it in the same arc - `daslib/aot_cpp.das` - the AOT C++ emitter lives here, NOT in C++ +- `nano/` - `libDaScriptNano`, the runtime with no compiler in it: a shadow include root whose four headers win over `include/`, so thirteen `src/` sources compile against a minimal `Context` unmodified. A source that needs an edit to build there is CARVED upstream, never forked (`nano/ARCHITECTURE.md`); every directory linking it must clear its inherited include dirs, which `nano/REVIEW.das` checks - `tests/aot/CMakeLists.txt` - register new test directories here for AOT compilation. Two AOT binaries: `test_aot_subset` (tests/language only, in ALL - the per-PR CI compile gate) and full `test_aot` (`EXCLUDE_FROM_ALL`, ~1080 AOT TUs - nightly CI + `preflight --full` only, via `--target test_aot`/`run_tests_aot`) - `dastest/` - test framework (used by both `tests/` and external repos) - `utils/detect-dupe/` (in-repo dupe finder) and `utils/find-dupe/` (Claude judge; needs `daspkg install --root utils/find-dupe` + `ANTHROPIC_API_KEY`) - both also MCP tools diff --git a/CMakeLists.txt b/CMakeLists.txt index 0024941ddd..709f32e11f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -934,8 +934,10 @@ include/vecmath/dag_vecMath.h include/vecmath/dag_vecMathDecl.h include/vecmath/dag_vecMath_common.h include/vecmath/dag_vecMath_const.h +include/vecmath/dag_vecMath_double.h include/vecmath/dag_vecMath_neon.h include/vecmath/dag_vecMath_pc_sse.h +include/vecmath/dag_vecMath_scalar.h include/vecmath/dag_vecMath_trig.h ) list(SORT VECMATH_SRC) @@ -1038,6 +1040,7 @@ src/misc/env_cfg.cpp src/misc/das_common.cpp src/misc/gc_node.cpp src/misc/globals.cpp +src/misc/hal.cpp src/misc/sysos.cpp src/misc/string_writer.cpp src/misc/memory_model.cpp @@ -1107,6 +1110,7 @@ src/misc/network.cpp src/simulate/hash.cpp src/simulate/debug_info.cpp src/simulate/runtime_string.cpp +src/simulate/escape_string.cpp src/simulate/runtime_iterator.cpp src/simulate/runtime_array.cpp src/simulate/runtime_table.cpp @@ -1116,6 +1120,8 @@ src/simulate/simulate.cpp src/simulate/simulate_exceptions.cpp src/simulate/simulate_gc.cpp src/simulate/simulate_gc_pod.cpp +src/simulate/builtin_array_ops.cpp +src/simulate/builtin_runtime_ops.cpp src/simulate/aot_library.cpp src/simulate/simulate_tracking.cpp src/simulate/simulate_visit.cpp @@ -1124,6 +1130,7 @@ src/simulate/simulate_fn_hash.cpp src/simulate/simulate_instrument.cpp include/daScript/simulate/cast.h include/daScript/simulate/annotation_arguments.h +src/simulate/annotation_arguments.cpp include/daScript/simulate/code_of_policies.h include/daScript/simulate/hash.h include/daScript/simulate/heap.h @@ -1229,6 +1236,11 @@ list(SORT DAS_LIB_SRC) SOURCE_GROUP_FILES("daslib" DSA_LIB_SRC) list(SORT DAS_LIB_SRC) +# libDaScriptNano - the minimal runtime. Added BEFORE the include_directories +# below on purpose: nano picks its own header search order, and inheriting this +# directory's would put the full include/ ahead of the headers nano shadows. +add_subdirectory(nano) + include_directories(include) include_directories(3rdparty/fmt/include) @@ -1615,6 +1627,10 @@ if (NOT ${DAS_TOOLS_DISABLED}) # Build daslang utilities as standalone executables (-exe mode) add_subdirectory(utils) + # Standalone-AOT examples on libDaScriptNano. They need daslang to generate + # their C++, which is why they live here and not beside the nano library. + add_subdirectory(examples/standalone) + endif() # This list should be significantly reduced, most of the files, except aot related should be private. @@ -1796,6 +1812,46 @@ install(FILES ${DAS_BUILTIN_HEADERS} DESTINATION include/daScript/builtin ) +# libDaScriptNano ships as sources, not as a built library: an embedder cross- +# compiles it for their own target with their own flags, which is the whole +# point of it. Its CMakeLists is self-contained given DASLANG_NANO_ROOT. +install(DIRECTORY ${PROJECT_SOURCE_DIR}/nano/ + DESTINATION nano + FILES_MATCHING + PATTERN "*.h" + PATTERN "*.cpp" + PATTERN "*.md" + PATTERN "CMakeLists.txt" + # REVIEW.md reviews a change to this repo's copy of nano; an SDK carries + # the library, not the process of changing it. (REVIEW.das is not matched + # by any pattern above, so it never ships either.) + REGEX "/REVIEW\\.md$" EXCLUDE +) +# The shared runtime sources nano compiles. They live under src/, which the SDK +# otherwise does not carry; nano/CMakeLists.txt names them one by one, and +# nano/REVIEW.das checks that this list still covers that one. +install(FILES + ${PROJECT_SOURCE_DIR}/src/misc/hal.cpp + ${PROJECT_SOURCE_DIR}/src/misc/memory_model.cpp + DESTINATION src/misc +) +install(FILES + ${PROJECT_SOURCE_DIR}/src/simulate/annotation_arguments.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/aot_library.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/builtin_array_ops.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/builtin_runtime_ops.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/data_walker.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/debug_info.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/escape_string.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/heap.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/runtime_array.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/runtime_iterator.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/runtime_table.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/simulate_gc_pod.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/standalone_ctx_utils.cpp + DESTINATION src/simulate +) + # Install all modules and main library. install(TARGETS libDaScript libDaScript_runtime libDaScriptDyn libDaScriptDyn_runtime libUriParser libUriParserDyn @@ -2094,6 +2150,11 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/examples/ DESTINATION ${DAS_INSTALL_EXAMPLESDIR} PATTERN "fatman" EXCLUDE PATTERN "modules" EXCLUDE + # standalone/ builds against a daslang TARGET in-tree and against an + # installed SDK from a bundle; the SDK copy is the .standalone.cmake below, + # renamed into place after this excludes the in-tree one. + PATTERN "standalone/CMakeLists.txt" EXCLUDE + PATTERN "CMakeLists.standalone.cmake" EXCLUDE PATTERN "_build" EXCLUDE PATTERN ".daspkg_cache" EXCLUDE PATTERN ".daspkg_tmp" EXCLUDE @@ -2109,6 +2170,13 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/examples/ PATTERN "*.sf2" EXCLUDE ) +# The SDK's copy of the standalone examples' build: same four targets, driven by +# find_package(DAS) instead of in-tree targets. +install(FILES ${PROJECT_SOURCE_DIR}/examples/standalone/CMakeLists.standalone.cmake + DESTINATION ${DAS_INSTALL_EXAMPLESDIR}/standalone + RENAME CMakeLists.txt +) + # The Fox rig/animation is CC-BY 4.0, so its attribution must also ship at the # bundle root beside the other licenses (the examples/gltf copy rides the blanket). install(FILES ${PROJECT_SOURCE_DIR}/examples/gltf/GLTF_SAMPLE_ASSETS.LICENSE diff --git a/ci/nano_arm_build.sh b/ci/nano_arm_build.sh new file mode 100755 index 0000000000..3b38d3f245 --- /dev/null +++ b/ci/nano_arm_build.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Cross-compile libDaScriptNano and the tier-A standalone example for a +# cortex-m4, and print the size of what comes out. +# +# THIS DOES NOT PASS YET, and it is not wired into CI. It is the acceptance test +# for the freestanding port, and running it names the work: newlib has no +# posix_memalign / malloc_usable_size / madvise, its libstdc++ is built without +# threads so declares nothing, its uint32_t is `unsigned long` (which +# makes every BitfieldAny and vec4 conversion ambiguous), and alloca +# needs its own include. Those live in platform.h, smart_ptr.h, arraytype.h and +# vectypes.h - shared headers every platform compiles, which is why the port is +# its own change rather than a corner of nano. +# +# Once it is green it becomes nano's drift tripwire: a change that pulls the +# compiler, fmt or the host's I/O back into the runtime fails here first. +# +# Usage: ci/nano_arm_build.sh [out-dir] +# Toolchain: apt-get install gcc-arm-none-eabi +set -euo pipefail + +DASLANG="${1:?usage: ci/nano_arm_build.sh [out-dir]}" +OUT="${2:-build/nano-arm}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CXX="${ARM_CXX:-arm-none-eabi-g++}" +SIZE="${ARM_SIZE:-arm-none-eabi-size}" + +cd "$ROOT" +mkdir -p "$OUT/generated" + +# nano/CMakeLists.txt is the one source list. Reading it here keeps this script +# from becoming a second one that drifts. The two variables it writes paths +# against mean different roots: nano's own sources sit under nano/, the shared +# ones under the repo root. +sources() { + sed -n '/^set(NANO_OWN_SRC/,/^)/p;/^set(NANO_SHARED_SRC/,/^)/p' nano/CMakeLists.txt \ + | grep -o '\${[A-Z_]*}/[^ ]*\.cpp' \ + | sed -e 's|^\${CMAKE_CURRENT_SOURCE_DIR}/|nano/|' \ + -e 's|^\${DASLANG_NANO_ROOT}/||' +} + +echo "== generating standalone C++ for examples/standalone/01_pure" +"$DASLANG" utils/aot/main.das -- -ctx \ + examples/standalone/01_pure/pure_math.das "$OUT/generated/" + +# -Os and no exceptions/RTTI is what an embedded target actually builds with; +# nano's das_config.h already forces DAS_ENABLE_EXCEPTIONS off, and this proves +# the compiler agrees. +ARM_FLAGS=( + -mcpu=cortex-m4 -mthumb -mfloat-abi=soft + -Os -ffunction-sections -fdata-sections + -fno-exceptions -fno-rtti -fno-threadsafe-statics + -std=c++17 + -I nano/include -I include -I "$OUT/generated" +) + +OBJS=() +echo "== compiling nano" +for src in $(sources); do + obj="$OUT/$(echo "$src" | tr '/' '_').o" + "$CXX" "${ARM_FLAGS[@]}" -c "$src" -o "$obj" + OBJS+=("$obj") +done + +echo "== compiling the generated context and the example" +"$CXX" "${ARM_FLAGS[@]}" -c "$OUT/generated/pure_math.das.cpp" -o "$OUT/pure_math.o" +"$CXX" "${ARM_FLAGS[@]}" -c examples/standalone/01_pure/main.cpp -o "$OUT/main.o" +OBJS+=("$OUT/pure_math.o" "$OUT/main.o") + +echo "== linking" +"$CXX" "${ARM_FLAGS[@]}" --specs=nosys.specs -Wl,--gc-sections \ + "${OBJS[@]}" -o "$OUT/nano_01_pure.elf" + +echo "== size (cortex-m4, -Os, gc-sections)" +"$SIZE" "$OUT/nano_01_pure.elf" diff --git a/examples/standalone/01_pure/main.cpp b/examples/standalone/01_pure/main.cpp new file mode 100644 index 0000000000..e39e6e3e79 --- /dev/null +++ b/examples/standalone/01_pure/main.cpp @@ -0,0 +1,59 @@ +// Tier A: a C++ program whose whole daslang dependency is libDaScriptNano. +// +// There is no daScript.h here, no Module, no compiler - just the header the +// standalone emitter generated. The context is a plain C++ object: construct +// it, call its methods, let it go out of scope. + +#include "daScript/nano_print.h" +#include "pure_math.das.h" + +#include + +using namespace das; + +static int failures = 0; + +static void expect_int ( const char * what, int have, int want ) { + if ( have != want ) { + printf("%s = %d, expected %d\n", what, have, want); + failures ++; + } +} + +static void expect_float ( const char * what, float have, float want ) { + const float d = have > want ? have - want : want - have; + if ( d > 1e-5f ) { + printf("%s = %f, expected %f\n", what, double(have), double(want)); + failures ++; + } +} + +static void to_console ( const char * text ) { + fputs(text, stdout); +} + +int main () { + // On a board this is where printk or a UART write goes. Setting it before + // the context exists means even a panic during construction is visible. + das_nano_set_print(&to_console); + + pure_math::Standalone ctx; + + pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f; + pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f; + + expect_float("dot(a,b)", ctx.dot(a, b), 32.0f); + + pure_math::Vec3 s = ctx.scale(a, 2.0f); + expect_float("scale(a,2).x", s.x, 2.0f); + expect_float("scale(a,2).z", s.z, 6.0f); + + expect_float("component(a,y)", ctx.component(a, pure_math::Axis::y), 2.0f); + expect_float("weighted_sum(a)", ctx.weighted_sum(a), 1.0f*0.25f + 2.0f*0.5f + 3.0f*0.25f); + + expect_int("collatz_steps(27)", ctx.collatz_steps(27), 111); + expect_int("collatz_steps(1)", ctx.collatz_steps(1), 0); + + printf(failures ? "01_pure: %d failure(s)\n" : "01_pure: ok\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/standalone/01_pure/pure_math.das b/examples/standalone/01_pure/pure_math.das new file mode 100644 index 0000000000..2eeef286b0 --- /dev/null +++ b/examples/standalone/01_pure/pure_math.das @@ -0,0 +1,65 @@ +options gen2 +options stack = 4096 + +// Tier A: pure POD compute. No das heap is ever touched here - no arrays, no +// tables, no strings, no closures - so the generated context needs only the +// stack and the constant data the standalone emitter puts in globals. +// +// `options stack = 4096` is honored exactly: the standalone context reserves +// this many bytes plus the headroom the global initializers need. + +struct Vec3 { + x : float + y : float + z : float +} + +enum Axis { + x + y + z +} + +let TAPS = fixed_array(0.25, 0.5, 0.25) + +[export] +def dot(a, b : Vec3) : float { + return a.x * b.x + a.y * b.y + a.z * b.z +} + +[export] +def scale(v : Vec3; k : float) : Vec3 { + return Vec3(x = v.x * k, y = v.y * k, z = v.z * k) +} + +[export] +def component(v : Vec3; axis : Axis) : float { + if (axis == Axis.x) { + return v.x + } elif (axis == Axis.y) { + return v.y + } + return v.z +} + +// A dim (fixed-size array) lives in globals or on the stack, never on the heap. +[export] +def weighted_sum(v : Vec3) : float { + let parts = fixed_array(v.x, v.y, v.z) + var total = 0.0 + for (p, w in parts, TAPS) { + total += p * w + } + return total +} + +[export] +def collatz_steps(n : int) : int { + var steps = 0 + var value = n + while (value > 1) { + value = value % 2 == 0 ? value / 2 : value * 3 + 1 + steps++ + } + return steps +} diff --git a/examples/standalone/02_heap/heap_demo.das b/examples/standalone/02_heap/heap_demo.das new file mode 100644 index 0000000000..084a80170b --- /dev/null +++ b/examples/standalone/02_heap/heap_demo.das @@ -0,0 +1,58 @@ +options gen2 +options stack = 16384 +options heap_size_hint = 65536 + +// Tier B: the das heap. Arrays, tables and `new`/`delete` all allocate from the +// context's own heap, sized by `options heap_size_hint` - a single malloc on a +// host, and on a board the one allocation the runtime makes. +// +// Nothing here uses strings: string interpolation and the string builders are +// the next tier up, and nano leaves them out on purpose. + +struct Sample { + id : int + value : float +} + +[export] +def sum_range(n : int) : int { + var values : array + values |> reserve(n) + for (i in range(n)) { + values |> push(i * i) + } + var total = 0 + for (v in values) { + total += v + } + return total +} + +[export] +def histogram_peak(n : int) : int { + var counts : table + for (i in range(n)) { + let bucket = i % 7 + counts[bucket] = (counts?[bucket] ?? 0) + 1 + } + var peak = 0 + for (c in values(counts)) { + if (c > peak) { + peak = c + } + } + return peak +} + +// An explicit `delete` frees through the scope-free path, the only part of the +// collector nano carries. There is no GC pass behind it: what a script does not +// delete stays allocated until the context does. +[export] +def alloc_and_free(id : int) : float { + var sample = new Sample(id = id, value = float(id) * 0.5) + let v = sample.value + unsafe { + delete sample + } + return v +} diff --git a/examples/standalone/02_heap/main.cpp b/examples/standalone/02_heap/main.cpp new file mode 100644 index 0000000000..12821522a5 --- /dev/null +++ b/examples/standalone/02_heap/main.cpp @@ -0,0 +1,50 @@ +// Tier B: the same nano runtime, now with the das heap in play. +// +// `options heap_size_hint = 65536` in heap_demo.das is what the context asks +// its allocator for up front. Everything the script allocates - the array, the +// table, the `new Sample` - comes out of that. + +#include "daScript/nano_print.h" +#include "heap_demo.das.h" + +#include + +using namespace das; + +static int failures = 0; + +static void expect_int ( const char * what, int have, int want ) { + if ( have != want ) { + printf("%s = %d, expected %d\n", what, have, want); + failures ++; + } +} + +static void to_console ( const char * text ) { + fputs(text, stdout); +} + +int main () { + das_nano_set_print(&to_console); + + heap_demo::Standalone ctx; + + // 0..9 squared + expect_int("sum_range(10)", ctx.sum_range(10), 285); + expect_int("sum_range(0)", ctx.sum_range(0), 0); + + // 20 values over 7 buckets: buckets 0..5 get 3, bucket 6 gets 2 + expect_int("histogram_peak(20)", ctx.histogram_peak(20), 3); + + expect_int("alloc_and_free(8)", int(ctx.alloc_and_free(8)), 4); + + // The heap is reused, not grown: running the same work again must not + // depend on how much ran before it. + for ( int i = 0; i != 100; ++i ) { + ctx.alloc_and_free(i); + } + expect_int("sum_range(10) after 100 alloc/free", ctx.sum_range(10), 285); + + printf(failures ? "02_heap: %d failure(s)\n" : "02_heap: ok\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/standalone/03_closures/closures.das b/examples/standalone/03_closures/closures.das new file mode 100644 index 0000000000..addc82fe14 --- /dev/null +++ b/examples/standalone/03_closures/closures.das @@ -0,0 +1,57 @@ +options gen2 +options stack = 32768 +options heap_size_hint = 65536 + +// Tier C: the shapes that need the function table at runtime. A lambda, a +// function pointer and a generator are all values that name a function by its +// mangled-name hash and look it up when invoked, so this is what proves the +// lookup tables the generated constructor fills are real on nano. + +[export] +def apply_twice(x : int) : int { + let add3 = @(v : int) : int => v + 3 + return invoke(add3, invoke(add3, x)) +} + +def double_it(x : int) : int { + return x + x +} + +[export] +def call_through_pointer(x : int) : int { + let fp = @@double_it + return invoke(fp, x) +} + +// A generator is a lambda with a resume point: each `yield` returns and the +// next call continues where it left off. The capture frame lives on the heap. +[export] +def sum_squares(n : int) : int { + var gen <- generator { + for (i in range(n)) { + yield i * i + } + return false + } + var total = 0 + for (v in gen) { + total += v + } + return total +} + +// A capturing closure. The capture frame is a heap allocation with a finalizer, +// so this is the shape that proves nano frees lambdas as well as calls them. +[export] +def count_up_to(limit : int) : int { + var count = 0 + unsafe { + let step <- @ capture(& count) { + count++ + } + while (count < limit) { + invoke(step) + } + } + return count +} diff --git a/examples/standalone/03_closures/main.cpp b/examples/standalone/03_closures/main.cpp new file mode 100644 index 0000000000..0245079941 --- /dev/null +++ b/examples/standalone/03_closures/main.cpp @@ -0,0 +1,47 @@ +// Tier C: lambdas, function pointers and generators on nano. +// +// Each of these looks a function up at runtime, through the tables the +// generated constructor fills in. Nothing about that path is compiled away. + +#include "daScript/nano_print.h" +#include "closures.das.h" + +#include + +using namespace das; + +static int failures = 0; + +static void expect_int ( const char * what, int have, int want ) { + if ( have != want ) { + printf("%s = %d, expected %d\n", what, have, want); + failures ++; + } +} + +static void to_console ( const char * text ) { + fputs(text, stdout); +} + +int main () { + das_nano_set_print(&to_console); + + closures::Standalone ctx; + + expect_int("apply_twice(10)", ctx.apply_twice(10), 16); + expect_int("call_through_pointer(21)", ctx.call_through_pointer(21), 42); + expect_int("sum_squares(5)", ctx.sum_squares(5), 30); + expect_int("sum_squares(0)", ctx.sum_squares(0), 0); + expect_int("count_up_to(7)", ctx.count_up_to(7), 7); + + // Capture frames and generator state are heap allocations with finalizers. + // Repeating the work must not leave any of them behind. + for ( int i = 0; i != 200; ++i ) { + ctx.sum_squares(5); + ctx.count_up_to(3); + } + expect_int("sum_squares(5) after 200 rounds", ctx.sum_squares(5), 30); + + printf(failures ? "03_closures: %d failure(s)\n" : "03_closures: ok\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/standalone/04_c_binding/blinker.das b/examples/standalone/04_c_binding/blinker.das new file mode 100644 index 0000000000..cb702de56b --- /dev/null +++ b/examples/standalone/04_c_binding/blinker.das @@ -0,0 +1,26 @@ +options gen2 +options stack = 16384 + +// Tier A + output. This is the shape an embedded program actually has: C owns +// the hardware and the loop, daslang owns the decision. Each tick the C side +// asks for a lamp pattern and writes whatever comes back to its GPIO register. +// +// `print` is the one call here that leaves the runtime. On a host it reaches +// stdout; on a board it reaches whatever das_nano_set_print was pointed at. +// Nothing else in nano does I/O. + +let LAMP_COUNT = 4 + +// A Larson scanner: one lit lamp bouncing between the ends. +[export] +def lamp_pattern(tick : int) : int { + let period = (LAMP_COUNT - 1) * 2 + let phase = tick % period + let position = phase < LAMP_COUNT ? phase : period - phase + return 1 << position +} + +[export] +def announce(tick, pattern : int) { + print("tick {tick}: lamps {pattern}\n") +} diff --git a/examples/standalone/04_c_binding/main.cpp b/examples/standalone/04_c_binding/main.cpp new file mode 100644 index 0000000000..f4cf59e96e --- /dev/null +++ b/examples/standalone/04_c_binding/main.cpp @@ -0,0 +1,48 @@ +// C owns the hardware, daslang owns the decision. +// +// This is the tier a microcontroller program lives in. The "GPIO register" here +// is a variable, but nothing else is pretend: the loop is C, the pattern comes +// from a compiled daslang function, and everything the script prints leaves +// through the one sink nano asks the embedder for. + +#include "daScript/nano_print.h" +#include "blinker.das.h" + +#include + +using namespace das; + +// Stand-in for a memory-mapped output register. +static unsigned g_gpio_out = 0; + +static void gpio_write ( unsigned bits ) { + g_gpio_out = bits; +} + +// On a board this is printk, HAL_UART_Transmit, or an append to a ring buffer. +static void board_print ( const char * text ) { + fputs(text, stdout); +} + +int main () { + das_nano_set_print(&board_print); + + blinker::Standalone ctx; + + // One full sweep of the scanner: 1,2,4,8,4,2 then back to 1. + static const unsigned expected[] = { 1, 2, 4, 8, 4, 2, 1, 2 }; + int failures = 0; + + for ( int tick = 0; tick != 8; ++tick ) { + const unsigned pattern = unsigned(ctx.lamp_pattern(tick)); + gpio_write(pattern); + ctx.announce(tick, int(pattern)); + if ( g_gpio_out != expected[tick] ) { + printf("tick %d wrote %u, expected %u\n", tick, g_gpio_out, expected[tick]); + failures ++; + } + } + + printf(failures ? "04_c_binding: %d failure(s)\n" : "04_c_binding: ok\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/standalone/CMakeLists.standalone.cmake b/examples/standalone/CMakeLists.standalone.cmake new file mode 100644 index 0000000000..f904e3f58a --- /dev/null +++ b/examples/standalone/CMakeLists.standalone.cmake @@ -0,0 +1,52 @@ +########################################################### +# Standalone-AOT examples on libDaScriptNano, built against +# an installed daslang SDK. +# +# cmake -DCMAKE_PREFIX_PATH= +# cmake --build . --config Release +# +# Each example compiles its .das to C++ with the SDK's daslang, then links +# that C++ and nano - a program with no compiler in it and no daslang binary +# at run time. +########################################################### + +cmake_minimum_required(VERSION 3.16) +project(daslang_standalone_examples CXX) + +find_package(DAS REQUIRED) + +# The SDK root, two levels up from lib/cmake/DAS/. +get_filename_component(DAS_SDK_ROOT "${DAS_DIR}/../../.." ABSOLUTE) +message(STATUS "daslang SDK root: ${DAS_SDK_ROOT}") + +# nano ships as sources; build it here with this project's flags. +add_subdirectory("${DAS_SDK_ROOT}/nano" nano_build) + +# nano decides the header search order for everything that links it, and a +# directory-level include_directories() is searched BEFORE any target's own. +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "") + +set(NANO_EXAMPLE_GEN "${CMAKE_CURRENT_BINARY_DIR}/_standalone_ctx_generated") +file(MAKE_DIRECTORY "${NANO_EXAMPLE_GEN}") + +function(das_nano_example name dir das_file) + set(_gen_cpp "${NANO_EXAMPLE_GEN}/${das_file}.cpp") + add_custom_command( + OUTPUT "${_gen_cpp}" "${NANO_EXAMPLE_GEN}/${das_file}.h" + COMMAND $ + "${DAS_SDK_ROOT}/utils/aot/main.das" + -- -ctx "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/${das_file}" + "${NANO_EXAMPLE_GEN}/" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/${das_file}" + COMMENT "Standalone AOT: ${das_file}" + VERBATIM + ) + add_executable(${name} "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/main.cpp" "${_gen_cpp}") + target_include_directories(${name} PRIVATE "${NANO_EXAMPLE_GEN}") + target_link_libraries(${name} PRIVATE libDaScriptNano) +endfunction() + +das_nano_example(standalone_01_pure 01_pure pure_math.das) +das_nano_example(standalone_02_heap 02_heap heap_demo.das) +das_nano_example(standalone_03_closures 03_closures closures.das) +das_nano_example(standalone_04_c_binding 04_c_binding blinker.das) diff --git a/examples/standalone/CMakeLists.txt b/examples/standalone/CMakeLists.txt new file mode 100644 index 0000000000..b72868fd9a --- /dev/null +++ b/examples/standalone/CMakeLists.txt @@ -0,0 +1,47 @@ +########################################################### +# Standalone-AOT examples built against libDaScriptNano. +# +# Each example is a .das compiled to C++ ahead of time plus a C++ main that +# links the generated code and nano - no daslang binary at run time, and no +# compiler in the linked program at all. +# +# SDK users build the same four targets from CMakeLists.standalone.cmake. +########################################################### + +# nano decides the header search order for everything that links it, and a +# directory-level include_directories() is searched BEFORE any target's own. +# Inheriting the root's would put the full include/ ahead of the headers nano +# shadows - the program would link nano while compiling against the full +# runtime's Context, which is a mismatch no test would name. +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "") + +set(NANO_EXAMPLE_GEN "${CMAKE_CURRENT_BINARY_DIR}/_standalone_ctx_generated") +file(MAKE_DIRECTORY "${NANO_EXAMPLE_GEN}") + +function(das_nano_example name dir das_file) + set(_gen_cpp "${NANO_EXAMPLE_GEN}/${das_file}.cpp") + add_custom_command( + OUTPUT "${_gen_cpp}" "${NANO_EXAMPLE_GEN}/${das_file}.h" + COMMAND $ + "${PROJECT_SOURCE_DIR}/utils/aot/main.das" + -- -ctx "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/${das_file}" + "${NANO_EXAMPLE_GEN}/" + DEPENDS daslang + "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/${das_file}" + "${PROJECT_SOURCE_DIR}/utils/aot/main.das" + "${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das" + "${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Standalone AOT: ${das_file}" + VERBATIM + ) + add_executable(${name} "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/main.cpp" "${_gen_cpp}") + target_include_directories(${name} PRIVATE "${NANO_EXAMPLE_GEN}") + target_link_libraries(${name} PRIVATE libDaScriptNano) + set_target_properties(${name} PROPERTIES FOLDER "examples/standalone") +endfunction() + +das_nano_example(standalone_01_pure 01_pure pure_math.das) +das_nano_example(standalone_02_heap 02_heap heap_demo.das) +das_nano_example(standalone_03_closures 03_closures closures.das) +das_nano_example(standalone_04_c_binding 04_c_binding blinker.das) diff --git a/include/daScript/simulate/annotation_arguments.h b/include/daScript/simulate/annotation_arguments.h index 29d4f66534..dd3ec78257 100644 --- a/include/daScript/simulate/annotation_arguments.h +++ b/include/daScript/simulate/annotation_arguments.h @@ -46,9 +46,9 @@ namespace das { struct DAS_API AnnotationArgumentList : AnnotationArguments { const AnnotationArgument * find ( const string & name, Type type ) const; bool getBoolOption(const string & name, bool def = false) const; - int32_t getIntOption(const string & name, int32_t def = false) const; - uint64_t getUInt64Option(const string & name, uint64_t def = false) const; - uint64_t getUInt64OptionEx (const string & name, const string & name2, uint64_t def = false) const; + int32_t getIntOption(const string & name, int32_t def = 0) const; + uint64_t getUInt64Option(const string & name, uint64_t def = 0) const; + uint64_t getUInt64OptionEx (const string & name, const string & name2, uint64_t def = 0) const; void serialize ( AstSerializer & ser ); }; } diff --git a/include/daScript/simulate/simulate.h b/include/daScript/simulate/simulate.h index 586c501a0c..158e29eadd 100644 --- a/include/daScript/simulate/simulate.h +++ b/include/daScript/simulate/simulate.h @@ -730,7 +730,7 @@ namespace das throw_error_at(line, "stack overflow while calling %s",fn->mangledName); } // fill prologue - auto aa = abiArg; auto acm = cmres; + auto aa = abiArg; auto acm = abiCMRES; abiArg = args; abiCMRES = cmres; #if DAS_SANITIZER memset(stack.sp(), 0xcd, fn->stackSize); diff --git a/install/CLAUDE.md b/install/CLAUDE.md index 0ce47c79bc..a1f4acefea 100644 --- a/install/CLAUDE.md +++ b/install/CLAUDE.md @@ -53,7 +53,7 @@ Task-specific instructions are in skill files under `skills/`. Read the relevant | `skills/mcp_tools.md` | Full MCP tool table + live-API reference | | `skills/das_formatting.md` | Creating or modifying any `.das` file | | `skills/comment_style_hygiene.md` | Writing or reviewing comments, names, or local code shape in ANY language | -| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums | +| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`) | | `skills/daslib_modules.md` | Using `daslib/` modules (linq, json, regex, etc.) | | `skills/das_macros.md` | Compile-time macros, AST manipulation, qmacro/quote, gc_node patterns | | `skills/daspkg.md` | Creating `.das_package` manifests, daspkg commands | diff --git a/nano/ARCHITECTURE.md b/nano/ARCHITECTURE.md new file mode 100644 index 0000000000..8c0e7bc20f --- /dev/null +++ b/nano/ARCHITECTURE.md @@ -0,0 +1,193 @@ +# libDaScriptNano - architecture + +## What nano is + +A daslang runtime with no compiler in it. Standalone AOT (`daslang utils/aot/main.das -- -ctx +script.das out/`) turns a script into C++ that an embedder compiles into their program; nano is +what that C++ links against instead of `libDaScript`. + +The full runtime is one library because the compiler and the runtime share headers. An embedder +who only wants to run already-compiled code still pays for the AST, the module registry, the +debugger, the serializer, fmt, and the STL those drag in. On a desktop that is a link-time +annoyance. On a microcontroller it is the difference between shipping and not. + +nano is not a fork of the runtime. It is the same runtime with a different set of headers in +front of it. + +## The mechanism: a shadow include root + +`nano/include` goes **before** `include/` in the header search order. A header that exists in +both resolves to nano's; every other header is the upstream file, byte for byte. + +That is the whole trick, and it buys the property that matters: the shared sources compile +against nano's `Context` without one `#ifdef`, and regular AOT output keeps its exact shape. A +build with `DAS_CONFIG_INCLUDE_DIR` (`cmake/das_config_eastl/`) already proved the pattern for +`das_config.h` alone; nano widens it to four headers. + +### The include-order trap + +CMake searches a directory's `include_directories()` **before** any target's own. A target that +links `libDaScriptNano` from a directory that inherited the root's `include_directories(include)` +compiles against the full runtime's `Context` while linking nano's - a mismatch with no +diagnostic, because most of the two agree. Every directory holding a nano-linked target clears +that property: + +```cmake +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "") +``` + +This repo's `nano/REVIEW.das` checks it mechanically, because it is the one mistake in this +design that produces a program rather than an error. + +## The include contract + +Generated standalone code includes exactly eight `daScript/...` paths, and nothing it emits +widens that set: + +``` +daScript/misc/platform.h +daScript/simulate/simulate.h +daScript/simulate/aot.h +daScript/simulate/aot_library.h +daScript/simulate/standalone_ctx_utils.h +daScript/simulate/bin_serializer.h // require $ +daScript/simulate/runtime_profile.h // require $ +daScript/misc/performance_time.h // require $ +``` + +The last three arrive because the `$` builtin module declares them, never because generated code +names a symbol from them. nano shadows all three with empty headers, which is compensation: the +real fix is for a standalone context not to emit those three includes at all, and it lands with +the next change to this folder. + +## What nano shadows + +`daScript/das_config.h` - the header everything else reaches the standard library through, which +is why replacing it re-points the whole runtime at a smaller set. It drops the STL headers the +runtime does not use, turns `DAS_ENABLE_EXCEPTIONS`, `DAS_DEBUGGER`, `DAS_FUSION`, +`DAS_BIND_EXTERNAL` and the crash handler off, and routes +`das_to_stdout_level_prefix_text` at the embedder's print sink. Those five are `#undef`ed and +redefined rather than defaulted with `#ifndef`, so a build that sets one on the command line +cannot silently get a runtime nano was not built to be - the panic path in particular is +`setjmp`/`longjmp`, not a C++ exception, and there is no second version of it here. + +`daScript/simulate/simulate.h` - the `Context` subset standalone AOT actually touches: a stack, +two heaps, the function and global tables, the three mangled-name lookups, and a panic path. +Gone: debug agents, stack walkers, GC roots, job-fork pools, the profiler, JIT hooks, +instrumentation, context cloning, code relocation, the init and shutdown scripts (the generated +constructor runs the init script itself). + +`daScript/ast/ast.h` - not the compiler front-end but the handful of names two reused runtime +sources want from it: `Annotation`, `TypeAnnotation`, and a `Module` with a name. + +The three `require $` headers above. + +## What nano owns + +`src/nano_context.cpp` - the `Context` implementation. Every function here has a counterpart in +`src/runtime/context.cpp` or `src/simulate/simulate_exceptions.cpp`. + +`src/nano_stubs.cpp` - the seams where nano ends, plus the smart-pointer tracking globals that +`src/misc/globals.cpp` owns upstream (that file also carries the job-queue globals, and +`job_que.h` is threads). + +`src/nano_string_writer.cpp` - `daScript/misc/string_writer.h` implemented over `snprintf`. The +header is upstream and unmodified; only fmt had to go, and it was reached through one template. + +## What nano reuses verbatim + +Thirteen sources compile straight out of `src/`, listed in `nano/CMakeLists.txt`. Adding one is a +decision: it must compile with no edit to the shared tree. When it needs an edit, the fix goes +upstream as a **carve** - splitting the runtime half of a file away from its compiler half - not +into a fork here. `src/simulate/simulate_gc_pod.cpp`, `src/simulate/annotation_arguments.cpp`, +`src/simulate/escape_string.cpp`, `src/simulate/builtin_array_ops.cpp` and +`src/simulate/builtin_runtime_ops.cpp` all exist because of that rule, and every one of them left +the full runtime better factored than it found it. + +## Fail-closed seams + +A stub that quietly does nothing is worse than no stub: on a board with no debugger, the moment +of the call is the only diagnosis anyone gets. Each of these stops the program instead: + +- `SimNode::copyNode`, `SimNode::visit`, `SimNode_CallBase::copyNode`, `SimVisitor::sub` - code + relocation and node dumping, neither of which nano has. +- `TypeAnnotation::walk` - walking a handled (C++-bound) type needs a registration only the + compiler makes. +- `makeAotJitNode` - a context generated with JIT nodes was generated for a different runtime. +- `FileInfo::serialize` - nano carries no serializer. +- `getSemanticHash` - nano carries no semantic hashes. + +`Context::setup` checks the `CodeOfPolicies` ABI stamp and stops on a mismatch. On nano both +sides are always built together, so a mismatch means the generated code and these headers came +from different daslang builds - the one failure that cannot be detected any later. + +Three neighbours of those seams answer instead of stopping, each matching what the full runtime +does when the thing it would consult is absent. `TypeInfo::resolveAnnotation` does nothing, +because resolving means asking a `Module`. `getCancelLimit` returns zero, which upstream means +"no cap" and is what an unbound environment returns there too. `print_current_stack_trace` is +empty; `os_debug_break` traps. + +## Panic + +Same contract as the full runtime built with `DAS_ENABLE_EXCEPTIONS=0`: fill in the message, +then `longjmp` to whoever armed `throwBuf`, or report and stop. One difference - the message +lands in a fixed 256-byte buffer rather than a `das::string`, because running out of heap is one +of the things that panics, and a longer message is truncated rather than allocated for. + +## Tiers + +A tier is what a script uses, not a build option: linking simply fails when a script reaches past +what nano carries. `examples/standalone/` has one example per tier, and this repo additionally +links all four into one program as the `nano_ctx` test. + +| tier | what it uses | example | +|---|---|---| +| A | POD compute - structs, enums, dims, no heap | `01_pure` | +| B | arrays, tables, `new`/`delete` through the scope-free path | `02_heap` | +| C | lambdas, function pointers, generators - the runtime function tables | `03_closures` | +| output | `print` reaching the embedder's sink | `04_c_binding` | + +Above these sits everything `src/simulate/runtime_string.cpp` provides - string interpolation and +the string builders - which nano leaves out because it is where fmt comes back. A script that +uses them fails to link, which is the boundary being honest rather than a program that silently +grew by a hundred kilobytes. + +Two builtin modules a host takes for granted are also absent, for the same reason: a script that +calls `max` needs `require math`, and the math module is a registration nano has no compiler to +make. + +## Ledgered cases + +**nano is not freestanding yet.** It builds where the full runtime builds, on the same +toolchains, and drops the compiler. Cross-compiling it for a bare-metal target does not work +today. What stands in the way, measured against arm-none-eabi with newlib: it has no +`posix_memalign`, `malloc_usable_size` or `madvise`; its libstdc++ is built without threads, so +`` declares nothing and neither `smart_ptr.h`'s ref-count lock nor this folder's +`contextMutex` compiles; its `uint32_t` is `unsigned long`, which makes every `BitfieldAny` +conversion and every `vec4` load ambiguous; and `alloca` needs its own include. Those +sit in `platform.h`, `smart_ptr.h`, `arraytype.h`, `vectypes.h` and `interop.h` - shared headers +every platform compiles - so the port is its own change, not a corner of nano. + +**`` and `` are still included.** `smart_ptr.h` declares a `static mutex` for +its ref-count tracking list and `memory_model.h` types its custom-grow hook as `das::function`. +Shadowing those two headers is part of the freestanding port above. + +**Floats print differently.** The full runtime formats through fmt, which prints the shortest +round-tripping form; nano prints `%g`. Same value, shorter text. This shows only in log output. + +**No stack walk on panic.** The prologue writes stay on, because the generated code's frame sizes +were computed assuming them, but nothing reads them - a panic reports its message and its +`LineInfo`. An unwinder is the obvious thing to add here and nobody has needed it yet. + +**No GC.** `simulate_gc_pod.cpp` gives explicit `delete` its scope-free path; there is no +collector behind it. What a script does not delete stays allocated until the context dies. For a +program with a fixed working set - which is what this tier is for - that is the correct trade, +but it is a trade. + +## Heap + +nano reuses `MemoryModel` and the heap allocators unchanged, so `options heap_size_hint` reaches +`setInitialSize` and the allocator makes one big-path allocation up front. `options +persistent_heap` picks `PersistentHeapAllocator` over the linear one, exactly as on a host. +`options stack = N` is honored exactly - the 16384 floor applies only when a script sets no stack +at all. diff --git a/nano/CMakeLists.txt b/nano/CMakeLists.txt new file mode 100644 index 0000000000..4f4cd3e7a1 --- /dev/null +++ b/nano/CMakeLists.txt @@ -0,0 +1,58 @@ +########################################################### +# libDaScriptNano - the minimal daslang runtime. +# +# The whole mechanism is the include order below: nano/include comes BEFORE the +# regular include/, so a header nano shadows wins and every other header is the +# upstream file, byte for byte. That is why NANO_SHARED_SRC can list sources +# straight out of src/ - they compile against nano's Context without knowing it. +# +# Adding a source here is a decision, not a convenience: it must compile with no +# edit to the shared tree. If it needs one, the fix goes upstream (a carve, the +# way src/simulate/simulate_gc_pod.cpp and src/simulate/annotation_arguments.cpp +# were carved out) rather than into a fork. +########################################################### + +# The daslang root: the tree in-repo, the SDK root once installed. +if(NOT DEFINED DASLANG_NANO_ROOT) + get_filename_component(DASLANG_NANO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +endif() + +set(NANO_OWN_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/src/nano_context.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/nano_stubs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/nano_string_writer.cpp +) + +set(NANO_SHARED_SRC + ${DASLANG_NANO_ROOT}/src/misc/hal.cpp + ${DASLANG_NANO_ROOT}/src/misc/memory_model.cpp + ${DASLANG_NANO_ROOT}/src/simulate/annotation_arguments.cpp + ${DASLANG_NANO_ROOT}/src/simulate/aot_library.cpp + ${DASLANG_NANO_ROOT}/src/simulate/builtin_array_ops.cpp + ${DASLANG_NANO_ROOT}/src/simulate/builtin_runtime_ops.cpp + ${DASLANG_NANO_ROOT}/src/simulate/data_walker.cpp + ${DASLANG_NANO_ROOT}/src/simulate/debug_info.cpp + ${DASLANG_NANO_ROOT}/src/simulate/escape_string.cpp + ${DASLANG_NANO_ROOT}/src/simulate/heap.cpp + ${DASLANG_NANO_ROOT}/src/simulate/runtime_array.cpp + ${DASLANG_NANO_ROOT}/src/simulate/runtime_iterator.cpp + ${DASLANG_NANO_ROOT}/src/simulate/runtime_table.cpp + ${DASLANG_NANO_ROOT}/src/simulate/simulate_gc_pod.cpp + ${DASLANG_NANO_ROOT}/src/simulate/standalone_ctx_utils.cpp +) + +add_library(libDaScriptNano STATIC ${NANO_OWN_SRC} ${NANO_SHARED_SRC}) + +target_include_directories(libDaScriptNano BEFORE PUBLIC + $ + $ + $ + $ +) + +target_compile_features(libDaScriptNano PUBLIC cxx_std_17) +set_target_properties(libDaScriptNano PROPERTIES FOLDER "nano") + +if(MSVC) + target_compile_options(libDaScriptNano PRIVATE /wd4100 /wd4127) +endif() diff --git a/nano/README.md b/nano/README.md new file mode 100644 index 0000000000..8dc72d0ec1 --- /dev/null +++ b/nano/README.md @@ -0,0 +1,79 @@ +# libDaScriptNano + +The daslang runtime with no compiler in it - for programs that run +already-compiled daslang and never compile any. + +You compile your script to C++ ahead of time on your workstation: + +```bash +daslang utils/aot/main.das -- -ctx my_script.das generated/ +``` + +and link the result against nano instead of `libDaScript`. The generated +`my_script.das.h` gives you a plain C++ class: + +```cpp +#include "daScript/nano_print.h" +#include "my_script.das.h" + +int main () { + das::das_nano_set_print(&my_log_sink); // where print goes + my_script::Standalone ctx; // construct it + int answer = ctx.my_exported_function(21); // call it +} +``` + +No `daScript.h`, no module registration, no `Program`, no file system. The +context is an object with a lifetime. + +## What it costs + +The four examples under `examples/standalone/`, built for x64 with MSVC, next to +the equivalent program on the full runtime: + +| program | tier | size | +|---|---|---| +| `01_pure` | POD compute, no heap | 120 KB | +| `02_heap` | arrays, tables, `new`/`delete` | 129 KB | +| `03_closures` | lambdas, function pointers, generators | 139 KB | +| `04_c_binding` | C drives the loop, das decides, `print` | 151 KB | +| full-runtime standalone context, for scale | | 463 KB | + +Those numbers include the platform's C runtime, so what they measure is the +difference nano makes on a host - not an embedded footprint. + +**nano does not cross-compile freestanding yet.** It runs on the platforms the +full runtime runs on, minus the compiler. Building it for a bare-metal target +needs portability work in the shared headers; `ARCHITECTURE.md` lists what. + +## What it leaves out + +A script that reaches past what nano carries fails to **link** - the boundary +announces itself at build time rather than becoming a program that quietly grew. +Absent: string interpolation and the string builders (this is where fmt comes +back), every builtin module including `math`, the GC, the debugger, the JIT, the +serializer, the profiler, threads, and the file system. + +`ARCHITECTURE.md` beside this file explains what nano is made of and lists the +trades it makes. + +## Building it + +In this repository nano builds as part of the normal CMake configure, and the +examples build with it. Outside it, point `DASLANG_NANO_ROOT` at an installed +SDK root and `add_subdirectory` this folder: + +```cmake +set(DASLANG_NANO_ROOT "/path/to/daslang-sdk") +add_subdirectory("${DASLANG_NANO_ROOT}/nano" nano_build) + +# Anything linking nano lets nano decide the header search order. +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "") +add_executable(my_program main.cpp generated/my_script.das.cpp) +target_include_directories(my_program PRIVATE generated) +target_link_libraries(my_program PRIVATE libDaScriptNano) +``` + +That `set_property` line is load-bearing, not tidiness: without it your target +compiles against the full runtime's headers while linking nano's library. +`ARCHITECTURE.md` says why under "The include-order trap". diff --git a/nano/REVIEW.das b/nano/REVIEW.das new file mode 100644 index 0000000000..400c9ee95f --- /dev/null +++ b/nano/REVIEW.das @@ -0,0 +1,140 @@ +options gen2 + +require strings +require daslib/fio +require dastest/review_gate + +// The mechanical half of nano/REVIEW.md (contract: REVIEW_COMMON.md at the repo root). +// Run from the repo root: bin/daslang nano/REVIEW.das - exit 0 clean, 1 with findings. + +let NANO_CMAKE = "nano/CMakeLists.txt" + +// Directories a checkout carries but git does not: build output, tool caches, and the +// nested worktrees agents run in. Walking them costs minutes and finds nothing. +let UNTRACKED_DIRS = { + ".git", ".claude", ".codex", ".vs", "build", "bin", "lib", + "node_modules", "out", "__pycache__", "_build" +} + +def private walk_cmakelists(root : string; var out : array) { + dir(root) $(name) { + return if (name == "." || name == "..") + let p = root == "." ? name : path_join(root, name) + let st = stat(p) + return if (!st.is_valid) + if (st.is_dir) { + return if (key_exists(UNTRACKED_DIRS, name)) + walk_cmakelists(p, out) + } elif (name == "CMakeLists.txt" || name == "CMakeLists.standalone.cmake") { + // the .standalone.cmake variants are installed AS CMakeLists.txt, so + // they carry the same obligation an SDK user will inherit + out |> push(to_generic_path(p)) + } + } +} + +// Sources the nano library compiles out of the shared tree, as CMake lists them. +def private nano_shared_sources(text : string) : array { + var inscope entries <- cmake_list_entries(text, "NANO_SHARED_SRC") + let marker = "}/" + return <- [for (e in entries); find(e, marker) >= 0 ? slice(e, find(e, marker) + length(marker), length(e)) : e] +} + +// Every shared source nano names is a file that is still there. A rename upstream would +// otherwise surface only when somebody builds nano. +def private check_shared_sources(text : string) { + for (src in nano_shared_sources(text)) { + continue if (fexist(src)) + gate_finding(NANO_CMAKE, "NANO_SHARED_SRC names {src}, which does not exist - the file moved or was renamed upstream") + } +} + +// A header under nano/include shadows an upstream header or is nano's own. A third case +// is a header nothing in the build will ever reach. +def private check_shadow_headers(folder : string; owned : array) { + dir(folder) $(name) { + return if (name == "." || name == "..") + let path = path_join(folder, name) + let st = stat(path) + return if (!st.is_valid) + if (st.is_dir) { + check_shadow_headers(path, owned) + return + } + return if (!(name |> ends_with(".h"))) + let generic = to_generic_path(path) + let upstream = "include/{slice(generic, length("nano/include/"), length(generic))}" + return if (fexist(upstream) || find_index(owned, name) >= 0) + gate_finding(generic, "shadows nothing: there is no {upstream}, and it is not one of nano's own headers - either the upstream header moved or this file is unreachable") + } +} + +// Paths the root CMakeLists installs into src/, as install(FILES ...) lists them. +def private installed_src_files(text : string) : array { + var out : array + for (blk in cmake_command_blocks(text, "install")) { + var inscope args <- cmake_args(blk) + continue if (find_index(args, "FILES") != 0) + var dest = "" + for (i in range(1, length(args))) { + if (args[i] == "DESTINATION" && i + 1 < length(args)) { + dest = args[i + 1] + } + } + continue if (!(dest |> starts_with("src/"))) + let marker = "}/" + out |> reserve(length(out) + length(args)) + for (a in args) { + let at = find(a, marker) + out |> push(at >= 0 ? slice(a, at + length(marker), length(a)) : a) + } + } + return <- out +} + +// An SDK carries no src/ except the sources nano compiles, so a source added to +// NANO_SHARED_SRC and not to the root install list builds here and not from an SDK. +def private check_shared_sources_ship(text : string) { + let root_cmake = "CMakeLists.txt" + var inscope installed <- installed_src_files(fread(root_cmake)) + for (src in nano_shared_sources(text)) { + continue if (find_index(installed, src) >= 0) + gate_finding(root_cmake, "{src} is in NANO_SHARED_SRC but no install(FILES ...) puts it under src/ - an SDK build of nano would not find it") + } +} + +// The include-order trap: CMake searches a directory's include_directories() before any +// target's own, so a nano-linked target in a directory that inherited the repo's would +// compile against the full runtime's headers while linking nano's library. +def private links_nano(text : string) : bool { + for (blk in cmake_command_blocks(text, "target_link_libraries")) { + var inscope args <- cmake_args(blk) + return true if (find_index(args, "libDaScriptNano") >= 0) + } + return false +} + +def private check_include_order_guard { + var files : array + walk_cmakelists(".", files) + files |> sort + for (f in files) { + let text = fread(f) + continue if (!links_nano(text) || find(text, "INCLUDE_DIRECTORIES \"\"") >= 0) + gate_finding(f, "links libDaScriptNano without clearing inherited include directories - add set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES \"\"), or the target compiles against the full runtime's headers") + } +} + +[export] +def main() : int { + if (!fexist(NANO_CMAKE)) { + to_log(LOG_ERROR, "nano/REVIEW.das: run from the repo root\n") + return 2 + } + let cmake_text = fread(NANO_CMAKE) + check_shared_sources(cmake_text) + check_shared_sources_ship(cmake_text) + check_shadow_headers("nano/include", ["nano_print.h"]) + check_include_order_guard() + return gate_verdict("nano") +} diff --git a/nano/REVIEW.md b/nano/REVIEW.md new file mode 100644 index 0000000000..679809a4ef --- /dev/null +++ b/nano/REVIEW.md @@ -0,0 +1,42 @@ +# nano Code Review Checklist + +**Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: +`ARCHITECTURE.md`. + +**A shared source added to `NANO_SHARED_SRC` compiles with no edit to the file itself.** +A source that needs an edit to build here is split upstream instead - its runtime half moved +into a file of its own - because an edited copy is a fork that drifts silently. + +**A source under `src/` that nano compiles must not include `daScript/ast/ast.h` for anything +beyond `Annotation`, `TypeAnnotation` and `Module::name`.** Those three are all nano's +`include/daScript/ast/ast.h` provides; anything else compiles here and means something +different than it does upstream. + +**A new stub in `src/nano_stubs.cpp` either implements the behaviour or stops the program.** +A stub that returns a default and continues turns a missing feature into a wrong answer on a +target with no debugger attached. + +**A directory holding a target that links `libDaScriptNano` clears its inherited include +directories.** CMake searches a directory's `include_directories()` before any target's own, +so without `set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "")` the target compiles +against the full runtime's headers while linking nano's library. + +**A name added to `include/daScript/simulate/simulate.h` here is a name something in the reuse +set or in generated code refers to.** This header is a subset, not a copy: a name added +speculatively is one nobody will know to remove. + +**A member kept in this folder's `Context` keeps the name and type it has in +`include/daScript/simulate/simulate.h` at the repo root.** Generated code and the reused +headers are written against those names, so a renamed member is a compile error at best and a +different field at worst. + +**A change to what nano leaves out updates the tier table in `ARCHITECTURE.md` and the +"What it leaves out" list in `README.md`.** Both are read by embedders deciding whether their +script fits. + +**A new tier or a new fail-closed seam ships an example under `examples/standalone/` and a +case in `tests-cpp/big/nano_ctx/test_nano_ctx.cpp`.** A tier with no linked program is a tier +that stops working without anything turning red. + +**A number in `README.md` was measured, not estimated.** The table says which toolchain and +which targets produced it, so a reader can reproduce it. diff --git a/nano/include/daScript/ast/ast.h b/nano/include/daScript/ast/ast.h new file mode 100644 index 0000000000..0e1d7fe6d7 --- /dev/null +++ b/nano/include/daScript/ast/ast.h @@ -0,0 +1,47 @@ +#pragma once + +// nano shadow of daScript/ast/ast.h. +// +// The real header is the compiler front-end: the AST, the type system, the +// module registry - tens of thousands of lines that a program which was already +// compiled has no use for. Two reused runtime sources still include it, and both +// want the same one thing from it: the type a handled (C++-bound) TypeInfo +// resolves to, so the data walker can hand a value to its annotation. +// +// nano has no modules, so nothing ever resolves, and the walk below stops the +// program instead of walking a type nobody described. BasicAnnotation itself +// comes from debug_info.h upstream; the two runtime includes are here because +// the real ast.h reaches Sequence and tableLiveSlot through its own chain, and +// the sources that include this file use them. + +#include "daScript/misc/platform.h" +#include "daScript/misc/string_writer.h" +#include "daScript/simulate/debug_info.h" +#include "daScript/simulate/annotation_arguments.h" +#include "daScript/simulate/runtime_iterator.h" +#include "daScript/simulate/runtime_table.h" + +namespace das { + + struct DataWalker; + + struct Module { + string name; + }; + + struct Annotation : BasicAnnotation { + Annotation ( const string & n, const string & cpn = "" ) : BasicAnnotation(n,cpn) {} + virtual bool rtti_isHandledTypeAnnotation() const { return false; } + Module * module = nullptr; + }; + + struct TypeAnnotation : Annotation { + TypeAnnotation ( const string & n, const string & cpn = "" ) : Annotation(n,cpn) {} + virtual bool rtti_isHandledTypeAnnotation() const override { return true; } + virtual size_t getSizeOf() const { return sizeof(void *); } + virtual size_t getAlignOf() const { return 1; } + virtual void walk ( DataWalker &, void * ) { + DAS_FATAL_ERROR("TypeAnnotation::walk: walking a handled type needs the full runtime, which nano is not"); + } + }; +} diff --git a/nano/include/daScript/das_config.h b/nano/include/daScript/das_config.h new file mode 100644 index 0000000000..28b127a8f3 --- /dev/null +++ b/nano/include/daScript/das_config.h @@ -0,0 +1,108 @@ +#pragma once + +// nano shadow of daScript/das_config.h. +// +// This header is the reason nano works at all: everything in include/ reaches +// the standard library through it, so replacing it - by putting nano/include +// ahead of include/ in the search order - re-points the whole runtime at a +// smaller set of headers without one #ifdef in the shared sources. +// +// Pre-C++20 inclusion order note: platform.h pulls this in before it defines +// __forceinline / DAS_SUPPRESS_UB / NO_ASAN_INLINE, so anything included here +// must not use those macros without its own fallback. + +#ifndef DAS_CUSTOM_HASH +#define DAS_CUSTOM_HASH 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace das {using namespace std;} + +#undef DAS_ENABLE_EXCEPTIONS +#define DAS_ENABLE_EXCEPTIONS 0 + +namespace das { + void das_throw(const char * msg); +} +#ifndef DAS_FMT_THROW_DEFINED +#define DAS_FMT_THROW_DEFINED +namespace das { + void das_stash_throw(const char * msg); + void das_throw_stashed(); + // the panic longjmp never unwinds (POSIX never does; the JIT stack cannot be + // unwound); the temporary dies when the stash statement ends - the jump + // crosses nothing live + template + inline void das_stash_then_throw(BuildAndStash && buildAndStash) { + buildAndStash(); + das_throw_stashed(); + } +} +#define FMT_THROW(x) das::das_stash_then_throw([&]{ das::das_stash_throw(((x).what())); }) +#endif + +#include +namespace das { +template , typename E = das::equal_to> +using das_map = das::daslang_hash_map; +template , typename E = das::equal_to> +using das_set = das::daslang_hash_set; +template , typename E = das::equal_to> +using das_hash_map = das::daslang_hash_map; +template , typename E = das::equal_to> +using das_hash_set = das::daslang_hash_set; +template , typename E = das::equal_to> +using das_insert_only_map = das::daslang_insert_only_hash_map; +template , typename E = das::equal_to> +using das_insert_only_set = das::daslang_insert_only_hash_set; +template , typename E = das::equal_to> +using das_insert_only_hash_map = das::daslang_insert_only_hash_map; +template , typename E = das::equal_to> +using das_insert_only_hash_set = das::daslang_insert_only_hash_set; +template +using das_safe_map = std::map; +template > +using das_safe_set = std::set; +} + +#define DAS_STD_HAS_BIND 1 + +#ifndef DAS_MAX_FUNCTION_ARGUMENTS +#define DAS_MAX_FUNCTION_ARGUMENTS 32 +#endif + +#undef DAS_FUSION +#define DAS_FUSION 0 +#undef DAS_DEBUGGER +#define DAS_DEBUGGER 0 +#undef DAS_BIND_EXTERNAL +#define DAS_BIND_EXTERNAL 0 +#undef DAS_USE_BASE_CRASH_HANDLER +#define DAS_USE_BASE_CRASH_HANDLER 0 + +#ifndef DAS_PRINT_VEC_SEPARATROR +#define DAS_PRINT_VEC_SEPARATROR "," +#endif + +namespace das { + void das_nano_write ( int level, const char * prefix, const char * text ); +} +#ifndef das_to_stdout_level_prefix_text +#define das_to_stdout_level_prefix_text(level, prefix, text) das::das_nano_write(int(level), prefix, text) +#endif diff --git a/nano/include/daScript/misc/performance_time.h b/nano/include/daScript/misc/performance_time.h new file mode 100644 index 0000000000..de3bda5067 --- /dev/null +++ b/nano/include/daScript/misc/performance_time.h @@ -0,0 +1,8 @@ +#pragma once + +// nano shadow of daScript/misc/performance_time.h - see simulate/bin_serializer.h +// for why the `require $` include set arrives here empty. +// +// The real header exposes ref_time_ticks / get_time_usec, which a freestanding +// target has no portable clock for. A script that actually calls them fails to +// link on nano rather than silently reading a stopped clock. diff --git a/nano/include/daScript/nano_print.h b/nano/include/daScript/nano_print.h new file mode 100644 index 0000000000..6bf960a4cf --- /dev/null +++ b/nano/include/daScript/nano_print.h @@ -0,0 +1,15 @@ +#pragma once + +// The one hook nano asks an embedder for. +// +// Everything the runtime prints - `print`, a panic report, a fatal - leaves +// through this sink. The default writes to stdout, which is right on a host and +// absent on a board; point it at printk, a UART write, or a ring buffer before +// constructing the context and the runtime needs no other I/O. + +namespace das { + + typedef void ( * das_nano_print_sink ) ( const char * text ); + + void das_nano_set_print ( das_nano_print_sink sink ); +} diff --git a/nano/include/daScript/simulate/bin_serializer.h b/nano/include/daScript/simulate/bin_serializer.h new file mode 100644 index 0000000000..d07c0c4208 --- /dev/null +++ b/nano/include/daScript/simulate/bin_serializer.h @@ -0,0 +1,8 @@ +#pragma once + +// nano shadow of daScript/simulate/bin_serializer.h. +// +// Generated standalone code includes this header because the `$` builtin module +// declares it, never because the emitted code names a symbol from it. The real +// header pulls the serializer and its STL containers into every generated TU; +// nano keeps the include valid and empty. diff --git a/nano/include/daScript/simulate/runtime_profile.h b/nano/include/daScript/simulate/runtime_profile.h new file mode 100644 index 0000000000..542a947dc8 --- /dev/null +++ b/nano/include/daScript/simulate/runtime_profile.h @@ -0,0 +1,4 @@ +#pragma once + +// nano shadow of daScript/simulate/runtime_profile.h - see bin_serializer.h for +// why the `require $` include set arrives here empty. diff --git a/nano/include/daScript/simulate/simulate.h b/nano/include/daScript/simulate/simulate.h new file mode 100644 index 0000000000..fa47da8701 --- /dev/null +++ b/nano/include/daScript/simulate/simulate.h @@ -0,0 +1,803 @@ +#pragma once + +// nano shadow of daScript/simulate/simulate.h. +// +// The real header carries the whole interpreter's Context: debug agents, stack +// walkers, GC roots, job-fork pools, the profiler, JIT hooks, instrumentation. +// Standalone AOT output touches none of that - it needs a stack, two heaps, the +// function and global tables, and a panic path. This header is that subset, +// with the names and member layout of the original so the reused headers +// (aot.h, heap.h, runtime_*.h, data_walker.h) compile against it unmodified. +// +// The rule for editing it: a name here exists because something in the reuse +// set or in generated code refers to it. Nothing is added speculatively, and +// nothing that IS here may be renamed - the reuse set is verbatim upstream. + +#include "daScript/misc/platform.h" +#include "daScript/misc/vectypes.h" +#include "daScript/misc/type_name.h" +#include "daScript/misc/arraytype.h" +#include "daScript/simulate/cast.h" +#include "daScript/simulate/runtime_string.h" +#include "daScript/simulate/debug_info.h" +#include "daScript/simulate/heap.h" +#include "daScript/simulate/code_of_policies.h" + +#include "daScript/simulate/simulate_visit_op.h" + +namespace das +{ + #define DAS_BIND_FUN(a) decltype(&a), a + #define DAS_BIND_MEMBER_FUN(a) decltype(&a), &a + #define DAS_BIND_PROP(BIGTYPE,FIELDNAME) decltype(&BIGTYPE::FIELDNAME), &BIGTYPE::FIELDNAME + #define DAS_BIND_FIELD(BIGTYPE,FIELDNAME) decltype(das::declval().FIELDNAME), offsetof(BIGTYPE,FIELDNAME) + + #define DAS_CALL_METHOD(mname) DAS_BIND_FUN(mname::invoke) + + #ifndef DAS_ENABLE_STACK_WALK + #define DAS_ENABLE_STACK_WALK 1 + #endif + + #define DAS_PROFILE_NODE + #define DAS_KEEPALIVE_CALL(context) + #define DAS_KEEPALIVE_LOOP(context) + + class Context; + struct SimNode; + struct Block; + struct SimVisitor; + + enum class ContextCategory : uint32_t { + none = 0 + , dead = (1<<0) + }; + + struct GlobalVariable { + char * name; + VarInfo * debugInfo; + SimNode * init; + uint64_t mangledNameHash; + uint32_t size; + uint32_t offset; + union { + struct { + bool shared : 1; + }; + uint32_t flags; + }; + }; + + struct SimFunction { + char * name; + char * mangledName; + SimNode * code; + FuncInfo * debugInfo; + uint64_t mangledNameHash; + void * aotFunction; + void * jitFunction; + uint32_t stackSize; + union { + uint32_t flags; + struct { + bool aot : 1; + bool fastcall : 1; + bool builtin : 1; + bool jit : 1; + bool unsafe : 1; + bool cmres : 1; + bool pinvoke : 1; + }; + }; + const LineInfo * getLineInfo() const; + }; + + struct DAS_API SimNode { + SimNode ( const LineInfo & at ) : debugInfo(at) {} + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ); + DAS_EVAL_ABI virtual vec4f eval ( Context & ) = 0; + virtual SimNode * visit ( SimVisitor & vis ); + virtual char * evalPtr ( Context & context ); + virtual bool evalBool ( Context & context ); + virtual float evalFloat ( Context & context ); + virtual double evalDouble ( Context & context ); + virtual int32_t evalInt ( Context & context ); + virtual uint32_t evalUInt ( Context & context ); + virtual int64_t evalInt64 ( Context & context ); + virtual uint64_t evalUInt64 ( Context & context ); + LineInfo debugInfo; + virtual bool rtti_node_isSourceBase() const { return false; } + virtual bool rtti_node_isBlock() const { return false; } + virtual bool rtti_node_isIf() const { return false; } + virtual bool rtti_node_isInstrument() const { return false; } + virtual bool rtti_node_isInstrumentFunction() const { return false; } + virtual bool rtti_node_isJit() const { return false; } + virtual bool rtti_node_isKeepAlive() const { return false; } + virtual bool rtti_node_isCallBase() const { return false; } + virtual bool rtti_node_isErrorMessage() const { return false; } + protected: + virtual ~SimNode() {} + }; + + template struct evalNode; + template <> struct evalNode { static __forceinline bool eval(Context & context, SimNode * node) { return node->evalBool(context); } }; + template <> struct evalNode { static __forceinline int32_t eval(Context & context, SimNode * node) { return node->evalInt(context); } }; + template <> struct evalNode { static __forceinline uint32_t eval(Context & context, SimNode * node) { return node->evalUInt(context); } }; + template <> struct evalNode { static __forceinline int64_t eval(Context & context, SimNode * node) { return node->evalInt64(context); } }; + template <> struct evalNode { static __forceinline uint64_t eval(Context & context, SimNode * node) { return node->evalUInt64(context); } }; + template <> struct evalNode { static __forceinline float eval(Context & context, SimNode * node) { return node->evalFloat(context); } }; + template <> struct evalNode { static __forceinline double eval(Context & context, SimNode * node) { return node->evalDouble(context); } }; + + struct alignas(16) Prologue { + union { + FuncInfo * info; + Block * block; + }; + union { + struct { + const char * fileName; + LineInfo * functionLine; + int32_t stackSize; + union { + uint32_t flags; + struct { + bool is_jit : 1; + }; + }; + }; + struct { + vec4f * arguments; + void * cmres; + LineInfo * line; + }; + }; + }; + + struct BlockArguments { + vec4f * arguments; + char * copyOrMoveResult; + }; + + enum EvalFlags : uint32_t { + stopForBreak = 1 << 0 + , stopForReturn = 1 << 1 + , stopForContinue = 1 << 2 + , jumpToLabel = 1 << 3 + , yield = 1 << 4 + }; + +#define DAS_PROCESS_LOOP_FLAGS_LABELED(beginLabel,endLabel,howtocontinue) \ + { if (context.stopFlags) { \ + if (context.stopFlags & EvalFlags::stopForContinue) { \ + context.stopFlags &= ~EvalFlags::stopForContinue; \ + howtocontinue; \ + } else if (context.stopFlags&EvalFlags::jumpToLabel && context.gotoLabeltotalLabels) { \ + if ((body=this->list+this->labels[context.gotoLabel])>=this->list) { \ + context.stopFlags &= ~EvalFlags::jumpToLabel; \ + goto beginLabel; \ + } \ + } \ + goto endLabel; \ + } } + +#define DAS_PROCESS_LOOP_FLAGS(howtocontinue) \ + DAS_PROCESS_LOOP_FLAGS_LABELED(loopbegin,loopend,howtocontinue) + +#define DAS_PROCESS_LOOP1_FLAGS(howtocontinue) \ + { if (context.stopFlags) { \ + if (context.stopFlags & EvalFlags::stopForContinue) { \ + context.stopFlags &= ~EvalFlags::stopForContinue; \ + howtocontinue; \ + } \ + goto loopend; \ + } } + +#define DAS_PROCESS_KEEPALIVE_LOOP1_FLAGS(howtocontinue) \ + DAS_PROCESS_LOOP1_FLAGS(howtocontinue) + + struct DAS_API SimVisitor { + virtual ~SimVisitor () = default; + virtual void preVisit ( SimNode * ) { } + virtual void cr () {} + virtual void op ( const char * /* name */, uint32_t /* sz */ = 0, const string & /* TT */ = string() ) {} + virtual void sp ( uint32_t /* stackTop */, const char * /* op */ = "#sp" ) { } + virtual void arg ( int32_t /* argV */, const char * /* argN */ ) { } + virtual void arg ( uint32_t /* argV */, const char * /* argN */ ) { } + virtual void arg ( const char * /* argV */, const char * /* argN */ ) { } + virtual void arg ( vec4f /* argV */, const char * /* argN */ ) { } + virtual void arg ( int64_t /* argV */, const char * /* argN */ ) { } + virtual void arg ( uint64_t /* argV */, const char * /* argN */ ) { } + virtual void arg ( float /* argV */, const char * /* argN */ ) { } + virtual void arg ( double /* argV */, const char * /* argN */ ) { } + virtual void arg ( bool /* argV */, const char * /* argN */ ) { } + virtual void arg ( Func /* fun */, const char * /* mangledName */, const char * /* argN */ ) { } + virtual void arg ( Func /* fun */, uint32_t /* mangledName */, const char * /* argN */ ) { } + virtual void sub ( SimNode ** nodes, uint32_t count, const char * ); + virtual SimNode * sub ( SimNode * node, const char * /* opN */ = "subexpr" ) { return node->visit(*this); } + virtual SimNode * visit ( SimNode * node ) { return node; } + }; + + DAS_API uint64_t getSemanticHash ( SimNode * node, Context * context ); + + class DAS_API Context { + public: + static constexpr uint32_t CONTEXT_MAGIC = 0xDA514C09; // "das" + "ctx" + version + uint32_t context_magic = CONTEXT_MAGIC; + Context(uint32_t stackSize = 16*1024, bool ph = false); + Context(const Context &) = delete; + Context & operator = (const Context &) = delete; + virtual ~Context(); + void setup(int totalVars, uint32_t globalStringHeapSize, CodeOfPolicies policies, AnnotationArgumentList options); + + uint64_t getGlobalSize() const { return globalsSize; } + uint64_t getSharedSize() const { return sharedSize; } + void updateSharedGlobalSize(uint64_t sharedDiff, uint64_t globalDiff) { + sharedSize += sharedDiff; + globalsSize += globalDiff; + } + + __forceinline char * allocateIterator ( uint64_t size, const char * iterName, const LineInfo * at ) { + auto aptr = heap->impl_allocateIterator(size, iterName); + if ( !aptr ) throw_out_of_memory(false, size + 16, at); + return aptr; + } + + __forceinline void freeIterator ( char * ptr, const LineInfo * ) { + heap->impl_freeIterator(ptr); + } + + __forceinline char * allocate ( uint64_t size, const LineInfo * at = nullptr ) { + auto aptr = heap->impl_allocate(size); + if ( !aptr && size ) throw_out_of_memory(false, size, at); + return aptr; + } + + __forceinline char * reallocate ( char * ptr, uint64_t oldSize, uint64_t size, const LineInfo * at ) { + auto aptr = heap->impl_reallocate(ptr, oldSize, size); + if ( !aptr && size ) throw_out_of_memory(false, size, at); + return aptr; + } + + __forceinline void free ( char * ptr, uint64_t size, const LineInfo * = nullptr ) { + heap->impl_free(ptr, size); + } + + __forceinline char * allocateString ( const char * text, uint64_t length, const LineInfo * at, bool = false ) { + auto astr = stringHeap->impl_allocateString(this, text, length, at); + if ( !astr && length ) throw_out_of_memory(true, length+1, at); + return astr; + } + + __forceinline char * allocateString ( const string & str, const LineInfo * at, bool = false ) { + auto astr = stringHeap->impl_allocateString(this, str.c_str(), uint64_t(str.size()), at); + if ( !astr && str.size() ) throw_out_of_memory(true, uint64_t(str.size()+1), at); + return astr; + } + + __forceinline char * allocateTempString ( const char * text, uint64_t length, const LineInfo * at ) { + return allocateString(text, length, at, /*temp*/true); + } + + __forceinline bool freeString ( char * ptr, uint64_t length, const LineInfo *, bool = false ) { + uint64_t size = length + 1; + size = (size + 15) & ~15; + if (stringHeap->isOwnPtr(ptr, size)) { + stringHeap->impl_freeString(ptr, length); + return true; + } + return false; + } + + __forceinline void freeTempString ( char * ptr, const LineInfo * at ) { + if ( stringHeap->isIntern() || stringHeap->isReclaimDisabled() ) return; + if ( stringDisposeQue ) freeString(stringDisposeQue,(uint64_t)strlen(stringDisposeQue),at, /*temp*/true); + stringDisposeQue = ptr; + } + + __forceinline void * getVariable ( int index ) const { + if ( uint32_t(index)=0 && indexfind(mnh); + DAS_ASSERT(it!=tabGMnLookup->end()); + return it->second; + } + __forceinline uint64_t adBySid ( uint64_t sid ) const { + auto it = tabAdLookup->find(sid); + DAS_ASSERT(it!=tabAdLookup->end()); + return it->second; + } + __forceinline SimFunction * fnByMangledName ( uint64_t mnh ) { + if ( mnh==0 ) return nullptr; + auto it = tabMnLookup->find(mnh); + return it!=tabMnLookup->end() ? it->second : nullptr; + } + + SimFunction * findFunction ( const char * name ) const; + SimFunction * findFunction ( const char * name, bool & isUnique ) const; + int findVariable ( const char * name ) const; + void stackWalk ( const LineInfo * at, bool showArguments, bool showLocalVariables ); + + virtual void to_out ( const LineInfo * at, int level, const char * message ); + void to_out ( const LineInfo * at, const char * message ) { + to_out(at, LogLevel::defaultPrint, message); + } + virtual void to_err ( const LineInfo * at, const char * message ) { + to_out(at, LogLevel::error, message); + } + virtual void breakPoint(const LineInfo & info, const char * reason = "breakpoint", const char * text = ""); + + __forceinline vec4f * abiArguments() { + return abiArg; + } + + __forceinline vec4f * abiThisBlockArguments() { + return abiThisBlockArg; + } + + __forceinline vec4f & abiResult() { + return result; + } + + __forceinline char * abiCopyOrMoveResult() { + return (char *) abiCMRES; + } + + DAS_EVAL_ABI __forceinline vec4f call(const SimFunction * fn, vec4f * args, LineInfo * line) { + // PUSH + char * EP, *SP; + if (!stack.push(fn->stackSize, EP, SP)) { + throw_error_at(line, "stack overflow while calling %s",fn->mangledName); + return v_zero(); + } + // fill prologue + auto aa = abiArg; + abiArg = args; +#if DAS_ENABLE_STACK_WALK + Prologue * pp = (Prologue *)stack.sp(); + pp->info = fn->debugInfo; + pp->arguments = args; + pp->cmres = nullptr; + pp->line = line; +#endif + // CALL + fn->code->eval(*this); + stopFlags = 0; + // POP + abiArg = aa; + stack.pop(EP, SP); + return result; + } + + DAS_EVAL_ABI __forceinline vec4f callOrFastcall(const SimFunction * fn, vec4f * args, LineInfo * line) { + if ( fn->fastcall ) { + auto aa = abiArg; + abiArg = args; + result = fn->code->eval(*this); + stopFlags = 0; + abiArg = aa; + return result; + } else { + return call(fn, args, line); + } + } + + DAS_EVAL_ABI __forceinline vec4f callWithCopyOnReturn(const SimFunction * fn, vec4f * args, void * cmres, LineInfo * line) { + // PUSH + char * EP, *SP; + if (!stack.push(fn->stackSize, EP, SP)) { + throw_error_at(line, "stack overflow while calling %s",fn->mangledName); + } + // fill prologue + auto aa = abiArg; auto acm = abiCMRES; + abiArg = args; abiCMRES = cmres; +#if DAS_ENABLE_STACK_WALK + Prologue * pp = (Prologue *)stack.sp(); + pp->info = fn->debugInfo; + pp->arguments = args; + pp->cmres = cmres; + pp->line = line; +#endif + // CALL + fn->code->eval(*this); + stopFlags = 0; + // POP + abiArg = aa; abiCMRES = acm; + stack.pop(EP, SP); + return result; + } + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4701) +#pragma warning(disable:4324) +#endif + + DAS_EVAL_ABI __forceinline vec4f invoke(const Block &block, vec4f * args, void * cmres, LineInfo * line ) { + char * EP, *SP; + vec4f * TBA = nullptr; + char * STB = stack.bottom(); +#if DAS_ENABLE_STACK_WALK + if (!stack.push_invoke(sizeof(Prologue), block.stackOffset, EP, SP)) { + throw_error_at(line, "stack overflow during invoke"); + } + Prologue * pp = (Prologue *)stack.ap(); + pp->block = (Block *)(intptr_t(&block) | 1); + pp->arguments = args; + pp->cmres = cmres; + pp->line = line; +#else + stack.invoke(block.stackOffset, EP, SP); +#endif + BlockArguments * __restrict ba = nullptr; + BlockArguments saveArguments; + if ( block.argumentsOffset || cmres ) { + ba = (BlockArguments *) ( STB + block.argumentsOffset ); + saveArguments = *ba; + ba->arguments = args; + ba->copyOrMoveResult = (char *) cmres; + TBA = abiThisBlockArg; + abiThisBlockArg = args; + } + vec4f * __restrict saveFunctionArguments = abiArg; + abiArg = block.functionArguments; + vec4f block_result = block.body->eval(*this); + abiArg = saveFunctionArguments; + if ( ba ) { + *ba = saveArguments; + abiThisBlockArg = TBA; + } + stack.pop(EP, SP); + return block_result; + } +#ifdef _MSC_VER +#pragma warning(pop) +#endif + + template + DAS_EVAL_ABI vec4f invokeEx(const Block &block, vec4f * args, void * cmres, Fn && when, LineInfo * line); + + __forceinline const char * getException() const { + return exception; + } + + void freeGlobalsAndShared(); + void allocateGlobalsAndShared(); + + __forceinline void singleStep ( const LineInfo &, bool ) { } + __forceinline bool isGlobalPtr ( char * ptr ) const { return globals<=ptr && ptr<(globals+globalsSize); } + __forceinline bool isSharedPtr ( char * ptr ) const { return shared<=ptr && ptr<(shared+sharedSize); } + public: + unique_ptr stringHeap; + unique_ptr heap; + shared_ptr constStringHeap; + shared_ptr code; + shared_ptr debugInfo; + char * stringDisposeQue = nullptr; + uint64_t * annotationData = nullptr; + char * globals = nullptr; + char * shared = nullptr; + StackAllocator stack; + uint32_t insideContext = 0; + bool persistent = false; + bool ownStack = false; + bool shutdown = false; + bool failed = false; + bool verySafeContext = false; // when true, array and table reserves don't free memory (unless the container's scratch flag or a scratch_* one-shot opts out) + uint64_t maxUnreservedSize = 64ull<<20; // mirrors CodeOfPolicies::max_unreserved_size + public: + vec4f * abiThisBlockArg; + vec4f * abiArg; + void * abiCMRES; + public: + LineInfo exceptionAt; + const char * exception = nullptr; + const char * last_exception = nullptr; + jmp_buf * throwBuf = nullptr; + static constexpr int EXCEPTION_MESSAGE_SIZE = 256; + char exceptionMessage[EXCEPTION_MESSAGE_SIZE] = {}; + protected: + GlobalVariable * globalVariables = nullptr; + SimFunction * functions = nullptr; + SimFunction ** initFunctions = nullptr; + uint64_t sharedSize = 0; + uint64_t globalsSize = 0; + uint32_t globalInitStackSize = 0; + int totalVariables = 0; + int totalFunctions = 0; + int totalInitFunctions = 0; + bool globalsOwner = true; + bool sharedOwner = true; + public: + SimNode * aotInitScript = nullptr; + public: + shared_ptr> tabMnLookup; + shared_ptr> tabGMnLookup; + shared_ptr> tabAdLookup; + public: + vec4f result; + uint32_t stopFlags = 0; + uint32_t gotoLabel = 0; + public: + recursive_mutex * contextMutex = nullptr; + public: + int32_t fnDepth = 0; + }; + + struct DataWalker; + +#define DAS_EVAL_NODE \ + EVAL_NODE(Ptr,char *); \ + EVAL_NODE(Int,int32_t); \ + EVAL_NODE(UInt,uint32_t); \ + EVAL_NODE(Int64,int64_t); \ + EVAL_NODE(UInt64,uint64_t); \ + EVAL_NODE(Float,float); \ + EVAL_NODE(Double,double); \ + EVAL_NODE(Bool,bool); + +#define DAS_NODE(TYPE,CTYPE) \ + DAS_EVAL_ABI virtual vec4f eval ( das::Context & context ) override { \ + return das::cast::from(compute(context)); \ + } \ + virtual CTYPE eval##TYPE ( das::Context & context ) override { \ + return compute(context); \ + } + +#define DAS_PTR_NODE DAS_NODE(Ptr,char *) +#define DAS_BOOL_NODE DAS_NODE(Bool,bool) +#define DAS_INT_NODE DAS_NODE(Int,int32_t) +#define DAS_FLOAT_NODE DAS_NODE(Float,float) +#define DAS_DOUBLE_NODE DAS_NODE(Double,double) + +#define DAS_SINGLE_STEP(context,at,forceStep) + + template + struct EvalTT { static __forceinline TT eval ( Context & context, SimNode * node ) { + return cast::to(node->eval(context)); }}; + template <> + struct EvalTT { static __forceinline int32_t eval ( Context & context, SimNode * node ) { + return node->evalInt(context); }}; + template <> + struct EvalTT { static __forceinline uint32_t eval ( Context & context, SimNode * node ) { + return node->evalUInt(context); }}; + template <> + struct EvalTT { static __forceinline int64_t eval ( Context & context, SimNode * node ) { + return node->evalInt64(context); }}; + template <> + struct EvalTT { static __forceinline uint64_t eval ( Context & context, SimNode * node ) { + return node->evalUInt64(context); }}; + template <> + struct EvalTT { static __forceinline float eval ( Context & context, SimNode * node ) { + return node->evalFloat(context); }}; + template <> + struct EvalTT { static __forceinline double eval ( Context & context, SimNode * node ) { + return node->evalDouble(context); }}; + template <> + struct EvalTT { static __forceinline bool eval ( Context & context, SimNode * node ) { + return node->evalBool(context); }}; + template <> + struct EvalTT { static __forceinline char * eval ( Context & context, SimNode * node ) { + return node->evalPtr(context); }}; + + // ERROR MESSAGE + struct DAS_API SimNode_WithErrorMessage : SimNode { + SimNode_WithErrorMessage ( const LineInfo & at, const char * em ) + : SimNode(at), errorMessage(em) {} + virtual bool rtti_node_isErrorMessage() const override { return true; } + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ) override; + const char * errorMessage = ""; + }; + + // FUNCTION CALL + struct DAS_API SimNode_CallBase : SimNode_WithErrorMessage { + SimNode_CallBase ( const LineInfo & at, const char * msg ) : SimNode_WithErrorMessage(at,msg) {} + virtual bool rtti_node_isCallBase() const override { return true; } + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ) override; + void visitCall ( SimVisitor & vis ); + __forceinline void evalArgs ( Context & context, vec4f * argValues ) { + for ( int i=0, is=nArguments; i!=is && !context.stopFlags; ++i ) { + argValues[i] = arguments[i]->eval(context); + } + } + SimNode * visitOp1 ( SimVisitor & vis, const char * op, int typeSize, const char * typeName ); + SimNode * visitOp2 ( SimVisitor & vis, const char * op, int typeSize, const char * typeName ); + SimNode * visitOp3 ( SimVisitor & vis, const char * op, int typeSize, const char * typeName ); +#define EVAL_NODE(TYPE,CTYPE)\ + virtual CTYPE eval##TYPE ( Context & context ) override { \ + return cast::to(eval(context)); \ + } + DAS_EVAL_NODE +#undef EVAL_NODE + SimNode ** arguments = nullptr; + TypeInfo ** types = nullptr; + SimFunction * fnPtr = nullptr; + int32_t nArguments = 0; + SimNode * cmresEval = nullptr; + void * aotFunction = nullptr; + }; + + struct DAS_API SimNode_Final : SimNode { + SimNode_Final ( const LineInfo & a ) : SimNode(a) {} + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ) override; + void visitFinal ( SimVisitor & vis ); + virtual SimNode * visit ( SimVisitor & vis ) override; + __forceinline void evalFinal ( Context & context ) { + if ( totalFinal ) { + auto SF = context.stopFlags; + auto RE = context.abiResult(); + context.stopFlags = 0; + for ( uint32_t i=0, is=totalFinal; i!=is; ++i ) { + finalList[i]->eval(context); + } + context.stopFlags = SF; + context.abiResult() = RE; + } + } + SimNode ** finalList = nullptr; + uint32_t totalFinal = 0; + }; + + struct DAS_API SimNode_Block : SimNode_Final { + SimNode_Block ( const LineInfo & at ) : SimNode_Final(at) {} + virtual bool rtti_node_isBlock() const override { return true; } + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ) override; + void visitBlock ( SimVisitor & vis ); + void visitLabels ( SimVisitor & vis ); + virtual SimNode * visit ( SimVisitor & vis ) override; + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + SimNode ** list = nullptr; + uint32_t total = 0; + uint64_t annotationDataSid = 0; + uint32_t * labels = nullptr; + uint32_t totalLabels = 0; + }; + + struct DAS_API SimNode_BlockNF : SimNode_Block { + SimNode_BlockNF ( const LineInfo & at ) : SimNode_Block(at) {} + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + }; + + struct DAS_API SimNode_BlockWithLabels : SimNode_Block { + SimNode_BlockWithLabels ( const LineInfo & at ) : SimNode_Block(at) {} + virtual SimNode * visit ( SimVisitor & vis ) override; + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + }; + + struct DAS_API SimNode_ForBase : SimNode_Block { + SimNode_ForBase ( const LineInfo & at ) : SimNode_Block(at) {} + SimNode * visitFor ( SimVisitor & vis, int total, const char * loopName ); + void allocateFor ( NodeAllocator * code, uint32_t t ); + virtual SimNode * copyNode ( Context & context, NodeAllocator * code ) override; + SimNode ** sources = nullptr; + uint32_t * strides = nullptr; + uint32_t * stackTop = nullptr; + uint32_t size; + uint32_t totalSources; + }; + + struct DAS_API SimNode_Delete : SimNode_WithErrorMessage { + SimNode_Delete ( const LineInfo & a, SimNode * s, uint32_t t, const char * em ) + : SimNode_WithErrorMessage(a,em), subexpr(s), total(t) {} + virtual SimNode * visit ( SimVisitor & vis ) override; + SimNode * subexpr; + uint32_t total; + }; + + struct DAS_API SimNode_ClosureBlock : SimNode_Block { + SimNode_ClosureBlock ( const LineInfo & at, bool nr, bool c0, uint64_t ad ) + : SimNode_Block(at), annotationData(ad), flags(0) { + this->needResult = nr; + this->code0 = c0; + } + virtual SimNode * visit ( SimVisitor & vis ) override; + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + uint64_t annotationData = 0; + union { + uint32_t flags; + struct { + bool needResult : 1; + bool code0 : 1; + }; + }; + }; + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4701) +#pragma warning(disable:4324) +#endif + template + DAS_EVAL_ABI vec4f Context::invokeEx(const Block &block, vec4f * args, void * cmres, Fn && when, LineInfo * line ) { + char * EP, *SP; + vec4f * TBA = nullptr; + char * STB = stack.bottom(); +#if DAS_ENABLE_STACK_WALK + if (!stack.push_invoke(sizeof(Prologue), block.stackOffset, EP, SP)) { + throw_error_at(line, "stack overflow during invokeEx"); + } + Prologue * pp = (Prologue *)stack.ap(); + pp->block = (Block *)(intptr_t(&block) | 1); + pp->arguments = args; + pp->cmres = cmres; + pp->line = line; +#else + stack.invoke(block.stackOffset, EP, SP); +#endif + BlockArguments * ba = nullptr; + BlockArguments saveArguments; + if ( block.argumentsOffset || cmres ) { + ba = (BlockArguments *) ( STB + block.argumentsOffset ); + saveArguments = *ba; + ba->arguments = args; + ba->copyOrMoveResult = (char *) cmres; + TBA = abiThisBlockArg; + abiThisBlockArg = args; + } + vec4f * __restrict saveFunctionArguments = abiArg; + abiArg = block.functionArguments; + SimNode_ClosureBlock * cb = (SimNode_ClosureBlock *) block.body; + when(cb->code0 ? cb->list[0] : block.body); + abiArg = saveFunctionArguments; + if ( ba ) { + *ba = saveArguments; + abiThisBlockArg = TBA; + } + stack.pop(EP, SP); + return result; + } +#ifdef _MSC_VER +#pragma warning(pop) +#endif +} + +#include "daScript/simulate/simulate_visit_op_undef.h" diff --git a/nano/src/nano_context.cpp b/nano/src/nano_context.cpp new file mode 100644 index 0000000000..efb676d679 --- /dev/null +++ b/nano/src/nano_context.cpp @@ -0,0 +1,198 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/simulate.h" +#include "daScript/simulate/aot.h" + +#include +#include +#include + +// The nano Context. Everything here has a counterpart in src/runtime/context.cpp +// and src/simulate/simulate_exceptions.cpp; what is missing is missing because +// standalone AOT never reaches it - no debug agents, no code relocation, no +// cloning, no init/shutdown scripts (the generated constructor calls the init +// script itself), no GC roots. + +namespace das { + + Context::Context(uint32_t stackSize, bool ph) : stack(stackSize) { + code = make_shared(); + constStringHeap = make_shared(); + debugInfo = make_shared(); + ownStack = (stackSize != 0); + persistent = ph; + } + + Context::~Context() { + freeGlobalsAndShared(); + if ( contextMutex ) { + delete contextMutex; + contextMutex = nullptr; + } + } + + void Context::setup(int totalVars, uint32_t globalStringHeapSize, CodeOfPolicies policies, AnnotationArgumentList options) { + if ( policies.abi_stamp != CodeOfPolicies::expected_abi_stamp() ) { + DAS_FATAL_ERROR("CodeOfPolicies ABI stamp mismatch at Context::setup: host wrote 0x%llx, this libDaScriptNano expects 0x%llx - the generated code and these headers came from different daslang builds", + (unsigned long long) policies.abi_stamp, + (unsigned long long) CodeOfPolicies::expected_abi_stamp()); + } + verySafeContext = options.getBoolOption("very_safe_context",policies.very_safe_context); + maxUnreservedSize = options.getUInt64Option("max_unreserved_size", policies.max_unreserved_size); + persistent = options.getBoolOption("persistent_heap", policies.persistent_heap); + if ( persistent ) { + heap = make_unique(); + stringHeap = make_unique(); + } else { + heap = make_unique(); + stringHeap = make_unique(); + } + heap->setInitialSize ( options.getIntOption("heap_size_hint", policies.heap_size_hint) ); + heap->setLimit ( options.getUInt64OptionEx("heap_size_limit", "max_heap_allocated", policies.max_heap_allocated) ); + stringHeap->setInitialSize ( options.getIntOption("string_heap_size_hint", policies.string_heap_size_hint) ); + stringHeap->setLimit ( options.getUInt64OptionEx("string_heap_size_limit", "max_string_heap_allocated", policies.max_string_heap_allocated) ); + constStringHeap = make_shared(); + totalVariables = totalVars; + if ( globalStringHeapSize ) { + constStringHeap->setInitialSize(globalStringHeapSize); + } + globalVariables = (GlobalVariable *) code->allocate( uint32_t(totalVars*sizeof(GlobalVariable)) ); + globalsSize = 0; + sharedSize = 0; + } + + void Context::freeGlobalsAndShared() { + if ( globals && globalsOwner ) { + das_aligned_free16(globals); + globals = nullptr; + } + if ( shared && sharedOwner ) { + das_aligned_free16(shared); + shared = nullptr; + } + } + + void Context::allocateGlobalsAndShared() { + freeGlobalsAndShared(); + globals = globalsSize ? (char *) das_aligned_alloc16(globalsSize) : nullptr; + shared = (sharedOwner && sharedSize) ? (char *) das_aligned_alloc16(sharedSize) : nullptr; + if ( shared ) memset(shared, 0, sharedSize); + globalsOwner = true; + sharedOwner = true; + } + + SimFunction * Context::findFunction ( const char * name ) const { + for ( int fni = 0; fni != totalFunctions; ++fni ) { + if ( strcmp(functions[fni].name, name)==0 ) { + return functions + fni; + } + } + return nullptr; + } + + SimFunction * Context::findFunction ( const char * name, bool & isUnique ) const { + SimFunction * found = nullptr; + isUnique = true; + for ( int fni = 0; fni != totalFunctions; ++fni ) { + if ( strcmp(functions[fni].name, name)==0 ) { + if ( found ) { + isUnique = false; + return found; + } + found = functions + fni; + } + } + return found; + } + + int Context::findVariable ( const char * name ) const { + for ( int vi = 0; vi != totalVariables; ++vi ) { + if ( strcmp(globalVariables[vi].name, name)==0 ) { + return vi; + } + } + return -1; + } + + void Context::to_out ( const LineInfo *, int level, const char * message ) { + if ( message ) { + das_to_stdout_level_prefix_text(level, getLogMarker(level), message); + } + } + + void Context::breakPoint(const LineInfo &, const char *, const char *) { + os_debug_break(); + } + + void Context::throw_fatal_error ( const char * message, const LineInfo & at ) { + const char * text = message ? message : ""; + size_t len = strlen(text); + if ( len > EXCEPTION_MESSAGE_SIZE - 2 ) len = EXCEPTION_MESSAGE_SIZE - 2; + memcpy(exceptionMessage, text, len); + exceptionMessage[len] = '\n'; + exceptionMessage[len+1] = 0; + exception = exceptionMessage; + exceptionAt = at; + if ( throwBuf ) { +#if defined(WIN64) || defined(_WIN64) + // "An invalid or unaligned stack was encountered during an unwind operation." + // is issued via longjmp - a known x64 issue; zeroing Frame disables unwinding + ((_JUMP_BUFFER *)throwBuf)->Frame = 0; +#endif + longjmp(*throwBuf,1); + } + to_err(&at, "\nunhandled exception\n"); + to_err(&at, exception); + breakPoint(at, "exception", exception); + exit(1); + } + + void Context::rethrow () { + if ( throwBuf ) { +#if defined(WIN64) || defined(_WIN64) + ((_JUMP_BUFFER *)throwBuf)->Frame = 0; +#endif + longjmp(*throwBuf,1); + } + to_err(nullptr, "\nunhandled exception\n"); + if ( exception ) to_err(nullptr, exception); + breakPoint(exceptionAt, "exception", exception ? exception : ""); + exit(1); + } + + void Context::throw_error ( const char * message ) { + throw_fatal_error(message, LineInfo()); + } + + void Context::throw_error_ex ( DAS_FORMAT_STRING_PREFIX const char * message, ... ) { + char buffer[EXCEPTION_MESSAGE_SIZE]; + va_list args; + va_start (args, message); + vsnprintf (buffer,EXCEPTION_MESSAGE_SIZE,message, args); + va_end (args); + throw_fatal_error(buffer, LineInfo()); + } + + void Context::throw_error_at ( const LineInfo * at, DAS_FORMAT_STRING_PREFIX const char * message, ... ) { + char buffer[EXCEPTION_MESSAGE_SIZE]; + va_list args; + va_start (args, message); + vsnprintf (buffer,EXCEPTION_MESSAGE_SIZE,message, args); + va_end (args); + throw_fatal_error(buffer, at ? *at : LineInfo()); + } + + void Context::throw_error_at ( const LineInfo & at, DAS_FORMAT_STRING_PREFIX const char * message, ... ) { + char buffer[EXCEPTION_MESSAGE_SIZE]; + va_list args; + va_start (args, message); + vsnprintf (buffer,EXCEPTION_MESSAGE_SIZE,message, args); + va_end (args); + throw_fatal_error(buffer, at); + } + + void Context::throw_out_of_memory ( bool isStringHeap, uint64_t size, const LineInfo * at ) { + throw_error_at(at, "out of %s memory, requested %llu bytes", + isStringHeap ? "string heap" : "heap", (unsigned long long) size); + } +} diff --git a/nano/src/nano_string_writer.cpp b/nano/src/nano_string_writer.cpp new file mode 100644 index 0000000000..6070790c24 --- /dev/null +++ b/nano/src/nano_string_writer.cpp @@ -0,0 +1,234 @@ +#include "daScript/misc/platform.h" +#include "daScript/misc/string_writer.h" +#include "daScript/nano_print.h" + +#include +#include + +// nano's implementation of daScript/misc/string_writer.h: the same header, +// numbers through snprintf instead of fmt, and one sink instead of the full +// runtime's re-pointable printer. nano/ARCHITECTURE.md says what that changes. + +namespace das { + + DAS_API StringWriterTag HEX; + DAS_API StringWriterTag DEC; + DAS_API StringWriterTag FIXEDFP; + DAS_API StringWriterTag SCIENTIFIC; + + mutex TextPrinter::pmut; + + static StringWriter & writeFormatted ( StringWriter & w, const char * fmt, ... ) { + char buf[128]; + va_list args; + va_start(args, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if ( n < 0 ) return w; + size_t len = size_t(n) < sizeof(buf) ? size_t(n) : sizeof(buf) - 1; + return w.writeStr(buf, len); + } + + StringWriter & StringWriter::writeStr(const char * st, size_t len) { + this->append(st, int(len)); + this->output(); + return *this; + } + StringWriter & StringWriter::writeChars(char ch, size_t len) { + if ( auto at = this->allocate(int(len)) ) { + memset(at, ch, len); + this->output(); + } + return *this; + } + StringWriter & StringWriter::write(const char * stst) { + if ( stst ) { + return writeStr(stst, strlen(stst)); + } else { + return *this; + } + } + StringWriter & StringWriter::operator << (const StringWriterTag & v ) { + if (&v == &HEX) hex = true; + else if (&v == &DEC) hex = false; + else if (&v == &FIXEDFP) fixed = true; + else if (&v == &SCIENTIFIC) fixed = false; + return *this; + } + StringWriter & StringWriter::operator << (char v) { return writeStr(&v, 1); } + StringWriter & StringWriter::operator << (unsigned char v) { return writeFormatted(*this, "%u", unsigned(v)); } + StringWriter & StringWriter::operator << (bool v) { return write(v ? "true" : "false"); } + StringWriter & StringWriter::operator << (int v) { return writeFormatted(*this, hex ? "%x" : "%d", v); } + StringWriter & StringWriter::operator << (long v) { return writeFormatted(*this, hex ? "%lx" : "%ld", v); } + StringWriter & StringWriter::operator << (long long v) { return writeFormatted(*this, hex ? "%llx" : "%lld", v); } + StringWriter & StringWriter::operator << (unsigned v) { return writeFormatted(*this, hex ? "%x" : "%u", v); } + StringWriter & StringWriter::operator << (unsigned long v) { return writeFormatted(*this, hex ? "%lx" : "%lu", v); } + StringWriter & StringWriter::operator << (unsigned long long v) { return writeFormatted(*this, hex ? "%llx" : "%llu", v); } + StringWriter & StringWriter::operator << (char * v) { return write(v ? (const char*)v : ""); } + StringWriter & StringWriter::operator << (const char * v) { return write(v ? v : ""); } + StringWriter & StringWriter::operator << (const string & v) { return v.length() ? writeStr(v.c_str(), v.length()) : *this; } + StringWriter & StringWriter::operator << (float v) { return writeFormatted(*this, fixed ? "%.9g" : "%g", double(v)); } + StringWriter & StringWriter::operator << (double v) { return writeFormatted(*this, fixed ? "%.17g" : "%g", v); } + + string FixedBufferTextWriter::str() const { + DAS_VERIFY(size <= DAS_SMALL_BUFFER_SIZE); + return string(data, size); + } + + uint64_t FixedBufferTextWriter::tellp() const { + return uint64_t(size); + } + + void FixedBufferTextWriter::append(const char * s, int l) { + if ( size + l <= DAS_SMALL_BUFFER_SIZE ) { + memcpy ( data+size, s, l ); + size += l; + } else { + DAS_FATAL_ERROR("DAS_SMALL_BUFFER_SIZE overflow"); + } + } + + char * FixedBufferTextWriter::allocate (int l) { + if ( size + l <= DAS_SMALL_BUFFER_SIZE ) { + char * res = data + size; + size += l; + return res; + } else { + DAS_FATAL_ERROR("DAS_SMALL_BUFFER_SIZE overflow"); + return nullptr; + } + } + + void FixedBufferTextWriter::output() { + } + + TextWriter::~TextWriter() { + if ( largeBuffer != fixedBuffer ) { + das_aligned_free16(largeBuffer); + } + } + + string TextWriter::str() const { + return string(largeBuffer, size); + } + + uint64_t TextWriter::tellp() const { + return uint64_t(size); + } + + bool TextWriter::empty() const { + return size == 0; + } + + char * TextWriter::data() { + return largeBuffer; + } + + void TextWriter::clear() { + size = 0; + } + + void TextWriter::output() { + } + + void TextWriter::append(const char * s, int l) { + char * at = allocate(l); + memcpy(at, s, l); + } + + char * TextWriter::c_str() { + if ( size < capacity ) { + largeBuffer[size] = 0; + return largeBuffer; + } else { + char * newBuffer = (char *) das_aligned_alloc16(size + 1); + memcpy(newBuffer, largeBuffer, size); + newBuffer[size] = 0; + if ( largeBuffer != fixedBuffer ) { + das_aligned_free16(largeBuffer); + } + largeBuffer = newBuffer; + capacity = size + 1; + return largeBuffer; + } + } + + char * TextWriter::allocate (int l) { + if ( size + l <= capacity ) { + char * res = largeBuffer + size; + size += l; + return res; + } else { + int32_t newCapacity = capacity * 2; + if ( newCapacity < size + l ) { + newCapacity = size + l; + } + char * newBuffer = (char *) das_aligned_alloc16(newCapacity); + if ( largeBuffer != fixedBuffer ) { + memcpy(newBuffer, largeBuffer, size); + das_aligned_free16(largeBuffer); + } else { + memcpy(newBuffer, fixedBuffer, size); + } + largeBuffer = newBuffer; + capacity = newCapacity; + char * res = largeBuffer + size; + size += l; + return res; + } + } + + void TextPrinter::output() { + lock_guard guard(pmut); + uint64_t newPos = tellp(); + if (newPos != pos) { + string st(data() + pos, size_t(newPos - pos)); + das_nano_write(LogLevel::defaultPrint, "", st.c_str()); + pos = newPos; + } + } + + const char * getLogMarker(int level) + { + if ( level >= LogLevel::error ) + return "[E] "; + else if ( level >= LogLevel::warning ) + return "[W] "; + else if ( level >= LogLevel::info ) + return "[I] "; + else + return ""; + } + + void LOG::output() { + auto newPos = tellp(); + if (newPos != pos) { + string st(data() + pos, size_t(newPos - pos)); + das_to_stdout_level_prefix_text(logLevel, useMarker ? getLogMarker(logLevel) : "", st.c_str()); + useMarker = false; + clear(); + pos = newPos = 0; + } + } + + void setTextPrinterSink ( TextPrinterSink sink ) { + das_nano_set_print(sink); + } + + void textPrinterToStderr() { + } + + bool textPrinterToFile ( const char * ) { + return false; + } +} + +void das_fatal_log ( const char * format, ... ) { + char buf[512]; + va_list args; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + buf[sizeof(buf) - 1] = 0; + das::das_nano_write(das::LogLevel::error, "", buf); +} diff --git a/nano/src/nano_stubs.cpp b/nano/src/nano_stubs.cpp new file mode 100644 index 0000000000..8441ce64d2 --- /dev/null +++ b/nano/src/nano_stubs.cpp @@ -0,0 +1,137 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/simulate.h" +#include "daScript/simulate/aot.h" +#include "daScript/simulate/aot_library.h" +#include "daScript/ast/ast.h" +#include "daScript/nano_print.h" + +#include + +// The seams where nano ends: the print sink, the storage src/misc/globals.cpp owns +// upstream, and the definitions that stop the program rather than pretend. Which +// is which, and why each one is here, is nano/ARCHITECTURE.md. + +namespace das { + + static void nano_default_print ( const char * text ) { + fputs(text, stdout); + } + + static das_nano_print_sink g_nano_sink = &nano_default_print; + + void das_nano_set_print ( das_nano_print_sink sink ) { + g_nano_sink = sink ? sink : &nano_default_print; + } + + void das_nano_write ( int level, const char * prefix, const char * text ) { + (void) level; + if ( prefix && *prefix ) g_nano_sink(prefix); + if ( text ) g_nano_sink(text); + } + + SimNode * SimNode::copyNode ( Context &, NodeAllocator * ) { + DAS_FATAL_ERROR("SimNode::copyNode: nano does not relocate code"); + return nullptr; + } + + SimNode * SimNode::visit ( SimVisitor & ) { + DAS_FATAL_ERROR("SimNode::visit: nano has no simulation visitor"); + return nullptr; + } + + bool SimNode::evalBool ( Context & context ) { return cast::to(eval(context)); } + float SimNode::evalFloat ( Context & context ) { return cast::to(eval(context)); } + double SimNode::evalDouble ( Context & context ) { return cast::to(eval(context)); } + int32_t SimNode::evalInt ( Context & context ) { return cast::to(eval(context)); } + uint32_t SimNode::evalUInt ( Context & context ) { return cast::to(eval(context)); } + int64_t SimNode::evalInt64 ( Context & context ) { return cast::to(eval(context)); } + uint64_t SimNode::evalUInt64 ( Context & context ) { return cast::to(eval(context)); } + char * SimNode::evalPtr ( Context & context ) { return cast::to(eval(context)); } + + SimNode * SimNode_WithErrorMessage::copyNode ( Context &, NodeAllocator * ) { + DAS_FATAL_ERROR("SimNode_WithErrorMessage::copyNode: nano does not relocate code"); + return nullptr; + } + + SimNode * SimNode_CallBase::copyNode ( Context &, NodeAllocator * ) { + DAS_FATAL_ERROR("SimNode_CallBase::copyNode: nano does not relocate code"); + return nullptr; + } + + void SimVisitor::sub ( SimNode **, uint32_t, const char * ) { + DAS_FATAL_ERROR("SimVisitor::sub: nano has no simulation visitor"); + } + + const LineInfo * SimFunction::getLineInfo() const { return &code->debugInfo; } + + uint64_t getSemanticHash ( SimNode *, Context * ) { + DAS_FATAL_ERROR("getSemanticHash: nano carries no semantic hashes"); + return 0; + } + + TypeAnnotation * TypeInfo::getAnnotation() const { + if ( type != Type::tHandle || !annotation_info ) return nullptr; + return (TypeAnnotation *) annotation_info->resolved; + } + + void TypeInfo::resolveAnnotation() const { + } + + uint64_t getCancelLimit() { + return 0; + } + + SimNode * makeAotJitNode ( Context &, void * ) { + DAS_FATAL_ERROR("makeAotJitNode: nano has no JIT - regenerate the standalone context without JIT"); + return nullptr; + } + + // debug_info.cpp defines each of these classes' first non-inline virtual, so + // it is where clang and gcc emit their vtables - and a vtable references every + // virtual whether or not anything calls it. Their real bodies live in TUs that + // need a file system and a serializer, so nano defines them here or does not + // link at all. MSVC hides this: it emits a vtable only where one is used. + void FileInfo::serialize ( AstSerializer & ) { + DAS_FATAL_ERROR("FileInfo::serialize: nano carries no serializer"); + } + + void TextFileInfo::serialize ( AstSerializer & ) { + DAS_FATAL_ERROR("TextFileInfo::serialize: nano carries no serializer"); + } + + void FileAccess::serialize ( AstSerializer & ) { + DAS_FATAL_ERROR("FileAccess::serialize: nano carries no serializer"); + } + + int64_t FileAccess::getFileMtime ( const string & ) const { + DAS_FATAL_ERROR("FileAccess::getFileMtime: nano has no file system"); + return -1; + } + + int64_t FileAccess::getFileSize ( const string & ) const { + DAS_FATAL_ERROR("FileAccess::getFileSize: nano has no file system"); + return -1; + } +} + +void os_debug_break() { +#if defined(_MSC_VER) + __debugbreak(); +#elif defined(__GNUC__) || defined(__clang__) + __builtin_trap(); +#endif +} + +void print_current_stack_trace() { +} + +namespace das { + uint64_t ptr_ref_count::ref_count_total = 0; + uint64_t ptr_ref_count::ref_count_track = 0; + uint64_t ptr_ref_count::ref_count_track_destructor = 0; + ptr_ref_count * ptr_ref_count::ref_count_head = nullptr; + mutex ptr_ref_count::ref_count_mutex; + + DAS_API atomic g_smart_ptr_total {0}; +} diff --git a/skills/cpp_integration.md b/skills/cpp_integration.md index 71ce2079dd..2a53276368 100644 --- a/skills/cpp_integration.md +++ b/skills/cpp_integration.md @@ -286,6 +286,35 @@ Generate stubs with `bin/daslang -aot input.das output.cpp`, adding `-aot-macros script defines macros. `error[50101]: AOT link failed on ` means the recorded hash no longer matches the source - regenerate and rebuild. +## Shipping without the compiler - `libDaScriptNano` + +A program that only ever RUNS daslang - never compiles any - does not need `libDaScript`. +Compile the script to C++ ahead of time (`daslang utils/aot/main.das -- -ctx script.das out/`) +and link the result against nano instead: + +```cpp +#include "daScript/nano_print.h" +#include "script.das.h" + +das::das_nano_set_print(&my_uart_write); // every print leaves through this +script::Standalone ctx; // a plain C++ object +int answer = ctx.exported_function(21); +``` + +No `daScript.h`, no `Program`, no module registration, no file system. Everything the runtime +prints goes to the one sink above, which is why this works on a target with no stdout. + +Three things to know before choosing it. **Anything the runtime does not carry fails to LINK, not +at run time** - string interpolation and the string builders, every builtin module including +`math`, the GC, the debugger, the JIT, the serializer, threads. **A target that links nano +decides its own header search order**: put `nano/include` ahead of `include/`, and in CMake add +`set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "")` to the directory holding the target, +or it compiles against the full runtime's headers while linking nano's library - a mismatch with +no diagnostic. **It is not freestanding yet**: it builds where the full runtime builds, minus the +compiler, and cross-compiling for bare metal still needs portability work in the shared headers. + +`nano/README.md` is the build recipe and `nano/ARCHITECTURE.md` lists what nano trades away. + ## Diagnostics - `TextPrinter`, never `fprintf(stderr, ...)` ```cpp diff --git a/src/ast/ast_module.cpp b/src/ast/ast_module.cpp index 812b78ba47..778bed5f38 100644 --- a/src/ast/ast_module.cpp +++ b/src/ast/ast_module.cpp @@ -59,37 +59,6 @@ namespace das { } } - // ANNOTATION - - const AnnotationArgument * AnnotationArgumentList::find ( const string & name, Type type ) const { - auto it = find_if(begin(), end(), [&](const AnnotationArgument & arg){ - return (arg.name==name) && (type==Type::tVoid || type==arg.type); - }); - return it==end() ? nullptr : &*it; - } - - bool AnnotationArgumentList::getBoolOption(const string & name, bool def) const { - auto arg = find(name, Type::tBool); - return arg ? arg->bValue : def; - } - - int32_t AnnotationArgumentList::getIntOption(const string & name, int32_t def) const { - auto arg = find(name, Type::tInt); - return arg ? arg->iValue : def; - } - - uint64_t AnnotationArgumentList::getUInt64Option(const string & name, uint64_t def) const { - auto arg = find(name, Type::tInt); - return arg ? uint64_t(arg->iValue) : def; - } - - uint64_t AnnotationArgumentList::getUInt64OptionEx(const string & name, const string & name2, uint64_t def) const { - auto arg = find(name, Type::tInt); - if (arg) return uint64_t(arg->iValue); - arg = find(name2, Type::tInt); - return arg ? uint64_t(arg->iValue) : def; - } - // MODULE void Module::addDependency ( Module * mod, bool pub ) { diff --git a/src/builtin/module_builtin_array.cpp b/src/builtin/module_builtin_array.cpp index 88df151c88..4d5ff4e841 100644 --- a/src/builtin/module_builtin_array.cpp +++ b/src/builtin/module_builtin_array.cpp @@ -10,163 +10,6 @@ namespace das { - int builtin_array_size ( const Array & arr ) { - // Always-on guard (panics in both debug + release). Use long_length() - // for arrays that may exceed INT_MAX elements — daslang's length() is - // int-returning and cannot represent the larger range without lying. - DAS_VERIFYF(arr.size <= uint64_t(INT32_MAX), "array size %llu exceeds INT_MAX; use long_length() instead", (unsigned long long)arr.size); - return int(arr.size); - } - - bool builtin_array_empty ( const Array & arr ) { - return arr.size == 0; - } - - int builtin_array_capacity ( const Array & arr ) { - DAS_VERIFYF(arr.capacity <= uint64_t(INT32_MAX), "array capacity %llu exceeds INT_MAX; use long_capacity() instead", (unsigned long long)arr.capacity); - return int(arr.capacity); - } - - int64_t builtin_array_long_size ( const Array & arr ) { - // The long_length surface returns int64; refuse to wrap negative if a host/interop - // path somehow produced a size > INT64_MAX. array_resize / array_grow already cap - // growth at INT64_MAX, so this catches embedder-side corruption. - DAS_VERIFYF(arr.size <= uint64_t(INT64_MAX), "array size %llu exceeds INT64_MAX", (unsigned long long)arr.size); - return int64_t(arr.size); - } - - int64_t builtin_array_long_capacity ( const Array & arr ) { - DAS_VERIFYF(arr.capacity <= uint64_t(INT64_MAX), "array capacity %llu exceeds INT64_MAX", (unsigned long long)arr.capacity); - return int64_t(arr.capacity); - } - - int builtin_array_lock_count ( const Array & arr ) { - return arr.lock; - } - - void builtin_array_resize ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %i", newSize); - array_resize ( *context, pArray, newSize, stride, /*zero*/ true, at ); - } - - void builtin_array_resize_no_init ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %i", newSize); - array_resize ( *context, pArray, newSize, stride, /*zero*/ false, at ); - } - - void builtin_array_reserve ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) return; // no point of displaying errors, if reserve fails - array_reserve( *context, pArray, newSize, stride, at ); - } - - void builtin_array_resize_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %lld", (long long)newSize); - array_resize ( *context, pArray, uint64_t(newSize), stride, /*zero*/ true, at ); - } - - void builtin_array_resize_no_init_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %lld", (long long)newSize); - array_resize ( *context, pArray, uint64_t(newSize), stride, /*zero*/ false, at ); - } - - void builtin_array_reserve_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) return; // no point of displaying errors, if reserve fails - array_reserve( *context, pArray, uint64_t(newSize), stride, at ); - } - - void builtin_array_erase ( Array & pArray, int index, int stride, Context * context, LineInfoArg * at ) { - if ( index < 0 || uint64_t(index) >= pArray.size ) { - context->throw_error_at(at, "erase index out of range, %d of %llu", index, (unsigned long long)pArray.size); - return; - } - memmove ( pArray.data+index*stride, pArray.data+(index+1)*stride, size_t(pArray.size-uint64_t(index)-1)*size_t(stride) ); - array_resize(*context, pArray, pArray.size-1, stride, false, at); - } - - void builtin_array_erase_range ( Array & pArray, int index, int count, int stride, Context * context, LineInfoArg * at ) { - // Compute end as uint64 sum AFTER non-negativity check to avoid signed overflow UB on index+count. - if ( index < 0 || count < 0 || uint64_t(index) + uint64_t(count) > pArray.size ) { - context->throw_error_at(at, "erasing array range is invalid: index=%d count=%d size=%llu", index, count, (unsigned long long)pArray.size); - return; - } - memmove ( pArray.data+uint64_t(index)*stride, pArray.data+(uint64_t(index)+uint64_t(count))*stride, size_t(pArray.size-uint64_t(index)-uint64_t(count))*size_t(stride) ); - array_resize(*context, pArray, pArray.size-uint64_t(count), stride, false, at); - } - - void builtin_array_erase_i64 ( Array & pArray, int64_t index, int stride, Context * context, LineInfoArg * at ) { - if ( index < 0 || uint64_t(index) >= pArray.size ) { - context->throw_error_at(at, "erase index out of range, %lld of %llu", (long long)index, (unsigned long long)pArray.size); - return; - } - memmove ( pArray.data+index*stride, pArray.data+(index+1)*stride, size_t(pArray.size-uint64_t(index)-1)*size_t(stride) ); - array_resize(*context, pArray, pArray.size-1, stride, false, at); - } - - void builtin_array_erase_range_i64 ( Array & pArray, int64_t index, int64_t count, int stride, Context * context, LineInfoArg * at ) { - // Compute end as uint64 sum AFTER non-negativity check to avoid signed overflow UB on index+count. - if ( index < 0 || count < 0 || uint64_t(index) + uint64_t(count) > pArray.size ) { - context->throw_error_at(at, "erasing array range is invalid: index=%lld count=%lld size=%llu", (long long)index, (long long)count, (unsigned long long)pArray.size); - return; - } - memmove ( pArray.data+uint64_t(index)*stride, pArray.data+(uint64_t(index)+uint64_t(count))*stride, size_t(pArray.size-uint64_t(index)-uint64_t(count))*size_t(stride) ); - array_resize(*context, pArray, pArray.size-uint64_t(count), stride, false, at); - } - - void builtin_array_clear ( Array & pArray, Context * context, LineInfoArg * at ) { - array_clear(*context, pArray, at); - } - - void builtin_array_lock ( Array & arr, Context * context, LineInfoArg * at ) { - array_lock(*context, arr, at); - } - - void builtin_array_unlock ( Array & arr, Context * context, LineInfoArg * at ) { - array_unlock(*context, arr, at); - } - - void builtin_array_lock_mutable ( const Array & arr, Context * context, LineInfoArg * at ) { - array_lock(*context, const_cast(arr), at); - } - - void builtin_array_unlock_mutable ( const Array & arr, Context * context, LineInfoArg * at ) { - array_unlock(*context, const_cast(arr), at); - } - - void builtin_array_clear_lock ( const Array & arr, Context * ) { - const_cast(arr).hopeless = true; - } - - void builtin_array_tag ( Array & arr, const char * name, Context * context ) { - // Debug helper: tag the array's current heap block with `name` so it shows - // up in heap reports under that name. Requires `options track_allocations` - // (the heap's mark_comment is a no-op otherwise). The tag is preserved - // across realloc by array_reserve, which reads the previous tag before - // overwriting with the generic "array" default. `name` is stored as-is - // in bigStuffComment; the caller owns its lifetime. The common case is - // a daslang literal (constStringHeap, never swept); dynamic daslang - // strings live in stringHeap, whose GC is skipped while track_allocations - // is on (see Context::collectHeap). - if ( arr.data && name ) context->heap->mark_comment(arr.data, name); - } - - void builtin_array_set_scratch ( Array & arr, bool value, Context * ) { - arr.scratch = value; - } - - bool builtin_array_is_scratch ( const Array & arr ) { - return arr.scratch; - } - - void builtin_array_scratch_reserve ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) return; // no point of displaying errors, if reserve fails - array_reserve_scratch( *context, pArray, newSize, stride, at ); - } - - void builtin_array_scratch_reserve_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { - if ( newSize<0 ) return; // no point of displaying errors, if reserve fails - array_reserve_scratch( *context, pArray, uint64_t(newSize), stride, at ); - } - void Module_BuiltIn::addArrayTypes(ModuleLibrary & lib) { // array functions // the public 'clear' is a builtin.das generic (finalize banner); this is its raw half diff --git a/src/builtin/module_builtin_runtime.cpp b/src/builtin/module_builtin_runtime.cpp index d2dc6f7bbd..ff476b642a 100644 --- a/src/builtin/module_builtin_runtime.cpp +++ b/src/builtin/module_builtin_runtime.cpp @@ -909,18 +909,6 @@ namespace das // core functions - void builtin_throw ( char * text, Context * context, LineInfoArg * at ) { - context->throw_error_at(at, "%s", text); - } - - void builtin_print ( char * text, Context * context, LineInfoArg * at ) { - context->to_out(at, text); - } - - void builtin_feint ( char *, Context *, LineInfoArg * ) { - // this function intentionally does nothing. its a fair replacement for the print, where we don't want print - } - void builtin_error ( char * text, Context * context, LineInfoArg * at ) { context->to_err(at, text); } @@ -1356,65 +1344,6 @@ namespace das result = { (Iterator *) iter }; } - struct LambdaIterator : Iterator { - using lambdaFunc = bool (*) (Context *,void*, char*); - LambdaIterator ( Context & context, const Lambda & ll, int st, LineInfo * at ) : Iterator(at), lambda(ll), stride(st) { - SimFunction ** fnMnh = (SimFunction **) lambda.capture; - if (!fnMnh) context.throw_error("invoke null lambda"); - simFunc = *fnMnh; - if (!simFunc) context.throw_error("invoke null function"); - aotFunc = (lambdaFunc) simFunc->aotFunction; - } - - DAS_SUPPRESS_UB - __forceinline bool InvokeLambda ( Context & context, char * ptr ) { - if ( aotFunc ) { - return (*aotFunc) ( &context, lambda.capture, ptr ); - } else { - vec4f argValues[4] = { - cast::from(lambda), - cast::from(ptr) - }; - auto res = context.call(simFunc, argValues, 0); - return cast::to(res); - } - } - virtual bool first ( Context & context, char * ptr ) override { - memset(ptr, 0, stride); - return InvokeLambda(context, ptr); - } - virtual bool next ( Context & context, char * ptr ) override { - return InvokeLambda(context, ptr); - } - virtual void close ( Context & context, char * ) override { - SimFunction ** fnMnh = (SimFunction **) lambda.capture; - SimFunction * finFunc = fnMnh[1]; - if (!finFunc) context.throw_error("generator finalizer is a null function"); - vec4f argValues[1] = { - cast::from(lambda.capture) - }; - auto flags = context.stopFlags; // need to save stop flags, we can be in the middle of some return or something - context.call(finFunc, argValues, 0); - context.freeIterator((char *)this, debugInfo); - context.stopFlags = flags; - } - virtual void walk ( DataWalker & walker ) override { - walker.beforeLambda(&lambda, lambda.getTypeInfo()); - walker.walk(lambda.capture, lambda.getTypeInfo()); - walker.afterLambda(&lambda, lambda.getTypeInfo()); - } - Lambda lambda; - SimFunction * simFunc = nullptr; - lambdaFunc aotFunc = nullptr; - int stride = 0; - }; - - void builtin_make_lambda_iterator ( Sequence & result, const Lambda lambda, int stride, Context * context, LineInfoArg * at ) { - char * iter = context->allocateIterator(sizeof(LambdaIterator), "lambda iterator", at); - new (iter) LambdaIterator(*context, lambda, stride, at); - result = { (Iterator *) iter }; - } - void resetProfiler( Context * context ) { context->resetProfiler(); } diff --git a/src/misc/hal.cpp b/src/misc/hal.cpp new file mode 100644 index 0000000000..b71404c794 --- /dev/null +++ b/src/misc/hal.cpp @@ -0,0 +1,15 @@ +#include "daScript/misc/platform.h" + +#include "daScript/misc/hal.h" + +// The out-of-line half of misc/hal.h. Only 32-bit MSVC needs one: there the +// header declares v_ldu_ptr instead of defining it, because that compiler +// generates flawed code for the inline form. Every other toolchain gets the +// inline definition and this file compiles to nothing. +// +// It lives here rather than in the simulator so that a build without one - the +// minimal runtime under nano/ - still resolves it. + +#if defined(_MSC_VER) && !defined(__clang__) && INTPTR_MAX == INT32_MAX +VECTORCALL vec4i v_ldu_ptr(const void * a) {return v_seti_x((int32_t)a);} +#endif diff --git a/src/simulate/annotation_arguments.cpp b/src/simulate/annotation_arguments.cpp new file mode 100644 index 0000000000..22fbffd3c7 --- /dev/null +++ b/src/simulate/annotation_arguments.cpp @@ -0,0 +1,37 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/annotation_arguments.h" + +#include + +namespace das { + + const AnnotationArgument * AnnotationArgumentList::find ( const string & name, Type type ) const { + auto it = find_if(begin(), end(), [&](const AnnotationArgument & arg){ + return (arg.name==name) && (type==Type::tVoid || type==arg.type); + }); + return it==end() ? nullptr : &*it; + } + + bool AnnotationArgumentList::getBoolOption(const string & name, bool def) const { + auto arg = find(name, Type::tBool); + return arg ? arg->bValue : def; + } + + int32_t AnnotationArgumentList::getIntOption(const string & name, int32_t def) const { + auto arg = find(name, Type::tInt); + return arg ? arg->iValue : def; + } + + uint64_t AnnotationArgumentList::getUInt64Option(const string & name, uint64_t def) const { + auto arg = find(name, Type::tInt); + return arg ? uint64_t(arg->iValue) : def; + } + + uint64_t AnnotationArgumentList::getUInt64OptionEx(const string & name, const string & name2, uint64_t def) const { + auto arg = find(name, Type::tInt); + if (arg) return uint64_t(arg->iValue); + arg = find(name2, Type::tInt); + return arg ? uint64_t(arg->iValue) : def; + } +} diff --git a/src/simulate/builtin_array_ops.cpp b/src/simulate/builtin_array_ops.cpp new file mode 100644 index 0000000000..92bc11d20c --- /dev/null +++ b/src/simulate/builtin_array_ops.cpp @@ -0,0 +1,172 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/simulate.h" +#include "daScript/simulate/runtime_array.h" +#include "daScript/simulate/aot_builtin.h" + +// The runtime half of daslang's array builtins. Registering them with the +// compiler is module_builtin_array.cpp's job and needs the AST; calling them +// needs nothing but the array and the context, which is why they live apart: +// AOT-generated code calls straight into these, and the minimal runtime +// (nano/) compiles this file with no compiler anywhere in the build. + +namespace das { + + int builtin_array_size ( const Array & arr ) { + // Always-on guard (panics in both debug + release). Use long_length() + // for arrays that may exceed INT_MAX elements — daslang's length() is + // int-returning and cannot represent the larger range without lying. + DAS_VERIFYF(arr.size <= uint64_t(INT32_MAX), "array size %llu exceeds INT_MAX; use long_length() instead", (unsigned long long)arr.size); + return int(arr.size); + } + + bool builtin_array_empty ( const Array & arr ) { + return arr.size == 0; + } + + int builtin_array_capacity ( const Array & arr ) { + DAS_VERIFYF(arr.capacity <= uint64_t(INT32_MAX), "array capacity %llu exceeds INT_MAX; use long_capacity() instead", (unsigned long long)arr.capacity); + return int(arr.capacity); + } + + int64_t builtin_array_long_size ( const Array & arr ) { + // The long_length surface returns int64; refuse to wrap negative if a host/interop + // path somehow produced a size > INT64_MAX. array_resize / array_grow already cap + // growth at INT64_MAX, so this catches embedder-side corruption. + DAS_VERIFYF(arr.size <= uint64_t(INT64_MAX), "array size %llu exceeds INT64_MAX", (unsigned long long)arr.size); + return int64_t(arr.size); + } + + int64_t builtin_array_long_capacity ( const Array & arr ) { + DAS_VERIFYF(arr.capacity <= uint64_t(INT64_MAX), "array capacity %llu exceeds INT64_MAX", (unsigned long long)arr.capacity); + return int64_t(arr.capacity); + } + + int builtin_array_lock_count ( const Array & arr ) { + return arr.lock; + } + + void builtin_array_resize ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %i", newSize); + array_resize ( *context, pArray, newSize, stride, /*zero*/ true, at ); + } + + void builtin_array_resize_no_init ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %i", newSize); + array_resize ( *context, pArray, newSize, stride, /*zero*/ false, at ); + } + + void builtin_array_reserve ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) return; // no point of displaying errors, if reserve fails + array_reserve( *context, pArray, newSize, stride, at ); + } + + void builtin_array_resize_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %lld", (long long)newSize); + array_resize ( *context, pArray, uint64_t(newSize), stride, /*zero*/ true, at ); + } + + void builtin_array_resize_no_init_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) context->throw_error_at(at, "resizing array to negative size %lld", (long long)newSize); + array_resize ( *context, pArray, uint64_t(newSize), stride, /*zero*/ false, at ); + } + + void builtin_array_reserve_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) return; // no point of displaying errors, if reserve fails + array_reserve( *context, pArray, uint64_t(newSize), stride, at ); + } + + void builtin_array_erase ( Array & pArray, int index, int stride, Context * context, LineInfoArg * at ) { + if ( index < 0 || uint64_t(index) >= pArray.size ) { + context->throw_error_at(at, "erase index out of range, %d of %llu", index, (unsigned long long)pArray.size); + return; + } + memmove ( pArray.data+index*stride, pArray.data+(index+1)*stride, size_t(pArray.size-uint64_t(index)-1)*size_t(stride) ); + array_resize(*context, pArray, pArray.size-1, stride, false, at); + } + + void builtin_array_erase_range ( Array & pArray, int index, int count, int stride, Context * context, LineInfoArg * at ) { + // Compute end as uint64 sum AFTER non-negativity check to avoid signed overflow UB on index+count. + if ( index < 0 || count < 0 || uint64_t(index) + uint64_t(count) > pArray.size ) { + context->throw_error_at(at, "erasing array range is invalid: index=%d count=%d size=%llu", index, count, (unsigned long long)pArray.size); + return; + } + memmove ( pArray.data+uint64_t(index)*stride, pArray.data+(uint64_t(index)+uint64_t(count))*stride, size_t(pArray.size-uint64_t(index)-uint64_t(count))*size_t(stride) ); + array_resize(*context, pArray, pArray.size-uint64_t(count), stride, false, at); + } + + void builtin_array_erase_i64 ( Array & pArray, int64_t index, int stride, Context * context, LineInfoArg * at ) { + if ( index < 0 || uint64_t(index) >= pArray.size ) { + context->throw_error_at(at, "erase index out of range, %lld of %llu", (long long)index, (unsigned long long)pArray.size); + return; + } + memmove ( pArray.data+index*stride, pArray.data+(index+1)*stride, size_t(pArray.size-uint64_t(index)-1)*size_t(stride) ); + array_resize(*context, pArray, pArray.size-1, stride, false, at); + } + + void builtin_array_erase_range_i64 ( Array & pArray, int64_t index, int64_t count, int stride, Context * context, LineInfoArg * at ) { + // Compute end as uint64 sum AFTER non-negativity check to avoid signed overflow UB on index+count. + if ( index < 0 || count < 0 || uint64_t(index) + uint64_t(count) > pArray.size ) { + context->throw_error_at(at, "erasing array range is invalid: index=%lld count=%lld size=%llu", (long long)index, (long long)count, (unsigned long long)pArray.size); + return; + } + memmove ( pArray.data+uint64_t(index)*stride, pArray.data+(uint64_t(index)+uint64_t(count))*stride, size_t(pArray.size-uint64_t(index)-uint64_t(count))*size_t(stride) ); + array_resize(*context, pArray, pArray.size-uint64_t(count), stride, false, at); + } + + void builtin_array_clear ( Array & pArray, Context * context, LineInfoArg * at ) { + array_clear(*context, pArray, at); + } + + void builtin_array_lock ( Array & arr, Context * context, LineInfoArg * at ) { + array_lock(*context, arr, at); + } + + void builtin_array_unlock ( Array & arr, Context * context, LineInfoArg * at ) { + array_unlock(*context, arr, at); + } + + void builtin_array_lock_mutable ( const Array & arr, Context * context, LineInfoArg * at ) { + array_lock(*context, const_cast(arr), at); + } + + void builtin_array_unlock_mutable ( const Array & arr, Context * context, LineInfoArg * at ) { + array_unlock(*context, const_cast(arr), at); + } + + void builtin_array_clear_lock ( const Array & arr, Context * ) { + const_cast(arr).hopeless = true; + } + + void builtin_array_tag ( Array & arr, const char * name, Context * context ) { + // Debug helper: tag the array's current heap block with `name` so it shows + // up in heap reports under that name. Requires `options track_allocations` + // (the heap's mark_comment is a no-op otherwise). The tag is preserved + // across realloc by array_reserve, which reads the previous tag before + // overwriting with the generic "array" default. `name` is stored as-is + // in bigStuffComment; the caller owns its lifetime. The common case is + // a daslang literal (constStringHeap, never swept); dynamic daslang + // strings live in stringHeap, whose GC is skipped while track_allocations + // is on (see Context::collectHeap). + if ( arr.data && name ) context->heap->mark_comment(arr.data, name); + } + + void builtin_array_set_scratch ( Array & arr, bool value, Context * ) { + arr.scratch = value; + } + + bool builtin_array_is_scratch ( const Array & arr ) { + return arr.scratch; + } + + void builtin_array_scratch_reserve ( Array & pArray, int newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) return; // no point of displaying errors, if reserve fails + array_reserve_scratch( *context, pArray, newSize, stride, at ); + } + + void builtin_array_scratch_reserve_i64 ( Array & pArray, int64_t newSize, int stride, Context * context, LineInfoArg * at ) { + if ( newSize<0 ) return; // no point of displaying errors, if reserve fails + array_reserve_scratch( *context, pArray, uint64_t(newSize), stride, at ); + } + +} diff --git a/src/simulate/builtin_runtime_ops.cpp b/src/simulate/builtin_runtime_ops.cpp new file mode 100644 index 0000000000..600b352b8b --- /dev/null +++ b/src/simulate/builtin_runtime_ops.cpp @@ -0,0 +1,87 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/simulate.h" +#include "daScript/simulate/aot.h" +#include "daScript/simulate/runtime_iterator.h" +#include "daScript/simulate/data_walker.h" + +// The runtime half of a few of daslang's core builtins - the ones AOT-generated +// code calls that need only a context. Their registration stays in +// module_builtin_runtime.cpp, where the compiler is; this file has no compiler +// in it, which is what lets the minimal runtime (nano/) compile it. + +namespace das { + + void builtin_throw ( char * text, Context * context, LineInfoArg * at ) { + context->throw_error_at(at, "%s", text); + } + + void builtin_print ( char * text, Context * context, LineInfoArg * at ) { + context->to_out(at, text); + } + + void builtin_feint ( char *, Context *, LineInfoArg * ) { + // this function intentionally does nothing. its a fair replacement for the print, where we don't want print + } + + + struct LambdaIterator : Iterator { + using lambdaFunc = bool (*) (Context *,void*, char*); + LambdaIterator ( Context & context, const Lambda & ll, int st, LineInfo * at ) : Iterator(at), lambda(ll), stride(st) { + SimFunction ** fnMnh = (SimFunction **) lambda.capture; + if (!fnMnh) context.throw_error("invoke null lambda"); + simFunc = *fnMnh; + if (!simFunc) context.throw_error("invoke null function"); + aotFunc = (lambdaFunc) simFunc->aotFunction; + } + + DAS_SUPPRESS_UB + __forceinline bool InvokeLambda ( Context & context, char * ptr ) { + if ( aotFunc ) { + return (*aotFunc) ( &context, lambda.capture, ptr ); + } else { + vec4f argValues[4] = { + cast::from(lambda), + cast::from(ptr) + }; + auto res = context.call(simFunc, argValues, 0); + return cast::to(res); + } + } + virtual bool first ( Context & context, char * ptr ) override { + memset(ptr, 0, stride); + return InvokeLambda(context, ptr); + } + virtual bool next ( Context & context, char * ptr ) override { + return InvokeLambda(context, ptr); + } + virtual void close ( Context & context, char * ) override { + SimFunction ** fnMnh = (SimFunction **) lambda.capture; + SimFunction * finFunc = fnMnh[1]; + if (!finFunc) context.throw_error("generator finalizer is a null function"); + vec4f argValues[1] = { + cast::from(lambda.capture) + }; + auto flags = context.stopFlags; // need to save stop flags, we can be in the middle of some return or something + context.call(finFunc, argValues, 0); + context.freeIterator((char *)this, debugInfo); + context.stopFlags = flags; + } + virtual void walk ( DataWalker & walker ) override { + walker.beforeLambda(&lambda, lambda.getTypeInfo()); + walker.walk(lambda.capture, lambda.getTypeInfo()); + walker.afterLambda(&lambda, lambda.getTypeInfo()); + } + Lambda lambda; + SimFunction * simFunc = nullptr; + lambdaFunc aotFunc = nullptr; + int stride = 0; + }; + + void builtin_make_lambda_iterator ( Sequence & result, const Lambda lambda, int stride, Context * context, LineInfoArg * at ) { + char * iter = context->allocateIterator(sizeof(LambdaIterator), "lambda iterator", at); + new (iter) LambdaIterator(*context, lambda, stride, at); + result = { (Iterator *) iter }; + } + +} diff --git a/src/simulate/escape_string.cpp b/src/simulate/escape_string.cpp new file mode 100644 index 0000000000..01869ba208 --- /dev/null +++ b/src/simulate/escape_string.cpp @@ -0,0 +1,39 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/runtime_string.h" + +namespace das { + + string escapeString ( const string & input, bool das_escape ) { + const char* str = input.c_str(); + const char* strEnd = str + input.length(); + string result; + result.reserve(input.size()); + for( ; str < strEnd; ++str ) { + auto ch = uint8_t(*str); + switch ( ch ) { + case '\"': result.append("\\\""); break; + case '\\': result.append("\\\\"); break; + case '\b': result.append("\\b"); break; + case '\v': result.append("\\v"); break; + case '\f': result.append("\\f"); break; + case '\n': result.append("\\n"); break; + case '\r': result.append("\\r"); break; + case '\t': result.append("\\t"); break; + case '{': if (das_escape) result.append("\\{"); else result.append("{"); break; + case '}': if (das_escape) result.append("\\}"); else result.append("}"); break; + default: + if ( ch <= 0x1f ) { + result.append("\\u00"); + const char tohex[] = "0123456789abcdef"; + result.append(1,tohex[ch>>4]); + result.append(1,tohex[ch&15]); + } else { + result.append(1, ch); + } + break; + } + } + return result; + } +} diff --git a/src/simulate/runtime_string.cpp b/src/simulate/runtime_string.cpp index d0de36777d..4c804461ed 100644 --- a/src/simulate/runtime_string.cpp +++ b/src/simulate/runtime_string.cpp @@ -459,39 +459,6 @@ namespace das return result; } - string escapeString ( const string & input, bool das_escape ) { - const char* str = input.c_str(); - const char* strEnd = str + input.length(); - string result; - result.reserve(input.size()); - for( ; str < strEnd; ++str ) { - auto ch = uint8_t(*str); - switch ( ch ) { - case '\"': result.append("\\\""); break; - case '\\': result.append("\\\\"); break; - case '\b': result.append("\\b"); break; - case '\v': result.append("\\v"); break; - case '\f': result.append("\\f"); break; - case '\n': result.append("\\n"); break; - case '\r': result.append("\\r"); break; - case '\t': result.append("\\t"); break; - case '{': if (das_escape) result.append("\\{"); else result.append("{"); break; - case '}': if (das_escape) result.append("\\}"); else result.append("}"); break; - default: - if ( ch <= 0x1f ) { - result.append("\\u00"); - const char tohex[] = "0123456789abcdef"; - result.append(1,tohex[ch>>4]); - result.append(1,tohex[ch&15]); - } else { - result.append(1, ch); - } - break; - } - } - return result; - } - static string getFewLines ( const char* st, uint32_t stlen, int ROW, int COL, int /*LROW*/, int LCOL, int TAB ) { TextWriter text; int col=0, row=1; diff --git a/src/simulate/simulate.cpp b/src/simulate/simulate.cpp index bdfe1bcf6a..44837983cc 100644 --- a/src/simulate/simulate.cpp +++ b/src/simulate/simulate.cpp @@ -929,8 +929,3 @@ namespace das const LineInfo * SimFunction::getLineInfo() const { return &code->debugInfo; } } - -//workaround compiler bug in MSVC 32 bit -#if defined(_MSC_VER) && !defined(__clang__) && INTPTR_MAX == INT32_MAX -VECTORCALL vec4i v_ldu_ptr(const void * a) {return v_seti_x((int32_t)a);} -#endif diff --git a/tests-cpp/big/nano_ctx/CMakeLists.txt b/tests-cpp/big/nano_ctx/CMakeLists.txt new file mode 100644 index 0000000000..ef756e0787 --- /dev/null +++ b/tests-cpp/big/nano_ctx/CMakeLists.txt @@ -0,0 +1,47 @@ +# Big test: the minimal runtime runs the same standalone contexts the full +# runtime does. Each of the four examples is its own tier - POD, heap, closures, +# output - and each one is here because a tier that stops linking is a tier that +# silently left nano, which no other test would notice. + +# nano decides the header search order for everything that links it, and a +# directory-level include_directories() is searched BEFORE any target's own. +# Inheriting tests-cpp's would put the full include/ ahead of the headers nano +# shadows, so this test would link nano while compiling against the full +# runtime's Context. +set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "") + +set(NANO_CTX_GEN "${CMAKE_CURRENT_BINARY_DIR}/_generated") +file(MAKE_DIRECTORY "${NANO_CTX_GEN}") + +set(NANO_CTX_EXAMPLES "${PROJECT_SOURCE_DIR}/examples/standalone") + +set(NANO_CTX_GENERATED) +foreach(_pair "01_pure/pure_math.das" "02_heap/heap_demo.das" + "03_closures/closures.das" "04_c_binding/blinker.das") + get_filename_component(_das_name "${_pair}" NAME) + add_custom_command( + OUTPUT "${NANO_CTX_GEN}/${_das_name}.cpp" "${NANO_CTX_GEN}/${_das_name}.h" + COMMAND $ + "${PROJECT_SOURCE_DIR}/utils/aot/main.das" + -- -ctx "${NANO_CTX_EXAMPLES}/${_pair}" "${NANO_CTX_GEN}/" + DEPENDS daslang + "${NANO_CTX_EXAMPLES}/${_pair}" + "${PROJECT_SOURCE_DIR}/utils/aot/main.das" + "${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das" + "${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Standalone AOT (nano): ${_das_name}" + VERBATIM + ) + list(APPEND NANO_CTX_GENERATED "${NANO_CTX_GEN}/${_das_name}.cpp") +endforeach() + +add_executable(test_nano_ctx test_nano_ctx.cpp ${NANO_CTX_GENERATED}) +target_include_directories(test_nano_ctx PRIVATE "${NANO_CTX_GEN}") +target_link_libraries(test_nano_ctx PRIVATE libDaScriptNano) +set_target_properties(test_nano_ctx PROPERTIES FOLDER "tests-cpp/big") + +add_test(NAME nano_ctx COMMAND test_nano_ctx + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) +set_tests_properties(nano_ctx PROPERTIES LABELS "big") +add_dependencies(test-big test_nano_ctx) diff --git a/tests-cpp/big/nano_ctx/test_nano_ctx.cpp b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp new file mode 100644 index 0000000000..12794593ca --- /dev/null +++ b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp @@ -0,0 +1,100 @@ +// All four standalone tiers, in one program, on the minimal runtime. +// +// The examples each run their own tier; this test links all four contexts into +// a single binary, which is the thing neither the examples nor the full-runtime +// standalone test cover: four generated contexts sharing one AOT registry. + +#include "daScript/nano_print.h" + +#include "pure_math.das.h" +#include "heap_demo.das.h" +#include "closures.das.h" +#include "blinker.das.h" + +#include + +using namespace das; + +static int failures = 0; + +static void expect_int ( const char * what, int have, int want ) { + if ( have != want ) { + printf("%s = %d, expected %d\n", what, have, want); + failures ++; + } +} + +static void expect_float ( const char * what, float have, float want ) { + const float d = have > want ? have - want : want - have; + if ( d > 1e-5f ) { + printf("%s = %f, expected %f\n", what, double(have), double(want)); + failures ++; + } +} + +// What the script prints, captured instead of printed, so the test can check it. +static string g_captured; + +static void capture_print ( const char * text ) { + g_captured += text; +} + +int main () { + das_nano_set_print(&capture_print); + + { // tier A - POD compute, no das heap + pure_math::Standalone ctx; + pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f; + pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f; + expect_float("dot", ctx.dot(a, b), 32.0f); + expect_float("scale.y", ctx.scale(a, 3.0f).y, 6.0f); + expect_float("component(z)", ctx.component(a, pure_math::Axis::z), 3.0f); + expect_float("weighted_sum", ctx.weighted_sum(a), 2.0f); + expect_int("collatz_steps(27)", ctx.collatz_steps(27), 111); + // `options stack = 4096` is honored exactly, plus the headroom the + // global initializers need - not rounded up to the 16k default. + expect_int("explicit stack is honored", ctx.stack.size() >= 4096 && ctx.stack.size() < 16384 ? 1 : 0, 1); + } + + { // tier B - the das heap + heap_demo::Standalone ctx; + expect_int("sum_range(10)", ctx.sum_range(10), 285); + expect_int("histogram_peak(20)", ctx.histogram_peak(20), 3); + expect_int("alloc_and_free(8)", int(ctx.alloc_and_free(8)), 4); + for ( int i = 0; i != 100; ++i ) ctx.alloc_and_free(i); + expect_int("sum_range after churn", ctx.sum_range(10), 285); + } + + { // tier C - lambdas, function pointers, generators + closures::Standalone ctx; + expect_int("apply_twice(10)", ctx.apply_twice(10), 16); + expect_int("call_through_pointer(21)", ctx.call_through_pointer(21), 42); + expect_int("sum_squares(5)", ctx.sum_squares(5), 30); + expect_int("count_up_to(7)", ctx.count_up_to(7), 7); + for ( int i = 0; i != 200; ++i ) { ctx.sum_squares(5); ctx.count_up_to(3); } + expect_int("sum_squares after churn", ctx.sum_squares(5), 30); + } + + { // output - `print` reaches the embedder's sink and nowhere else + blinker::Standalone ctx; + expect_int("lamp_pattern(3)", ctx.lamp_pattern(3), 8); + expect_int("lamp_pattern(4)", ctx.lamp_pattern(4), 4); + g_captured.clear(); + ctx.announce(2, ctx.lamp_pattern(2)); + expect_int("print reached the sink", g_captured == "tick 2: lamps 4\n" ? 1 : 0, 1); + if ( g_captured != "tick 2: lamps 4\n" ) { + printf(" captured: \"%s\"\n", g_captured.c_str()); + } + } + + { // a handled TypeInfo an embedder built by hand answers "unknown" rather + // than dereferencing a null AnnotationInfo, matching the full runtime + TypeInfo handled = {}; + handled.type = Type::tHandle; + handled.annotation_info = nullptr; + expect_int("getAnnotation on a null annotation_info", handled.getAnnotation() == nullptr ? 1 : 0, 1); + } + + printf(failures ? "nano_ctx: %d failure(s)\n" : "nano_ctx: ok\n", failures); + return failures ? 1 : 0; +}