From 5967f29c704a167ee65d6fa728d34df01f13545c Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Wed, 11 Feb 2026 15:57:53 -0800 Subject: [PATCH] Add WIP bytecode VM. update readme init add a test.sh script get a working test class-ify test runner flesh out test framework clean up test runner with test names clean move test code into a header-only implementation ensure all functions in header are inline move test framework code to github.com/christopherfujino/test.hpp create a chunk update docs move implementation to cpp file start debugging have .debug() return string rather than print to stdout move code out of header file format add Value type format --- README.md | 35 ++++- README.tmpl.md | 5 +- dbc.json | 4 +- dune-project | 4 +- lib/interpreter/native.ml | 2 +- native/cpp/bytecode/.clangd | 4 + native/cpp/bytecode/CMakeLists.txt | 38 +++++ native/cpp/bytecode/bytecode.cpp | 37 +++++ native/cpp/bytecode/bytecode.hpp | 40 +++++ native/cpp/bytecode/configure.sloth | 31 ++++ native/cpp/bytecode/test.cpp | 19 +++ native/cpp/bytecode/test.sh | 10 ++ native/dune | 2 +- sloth_script.opam | 4 +- test/green_specs/configure_cmake.sloth | 197 +++++++++++++++++++++++++ 15 files changed, 417 insertions(+), 15 deletions(-) create mode 100644 native/cpp/bytecode/.clangd create mode 100644 native/cpp/bytecode/CMakeLists.txt create mode 100644 native/cpp/bytecode/bytecode.cpp create mode 100644 native/cpp/bytecode/bytecode.hpp create mode 100755 native/cpp/bytecode/configure.sloth create mode 100644 native/cpp/bytecode/test.cpp create mode 100755 native/cpp/bytecode/test.sh create mode 100644 test/green_specs/configure_cmake.sloth diff --git a/README.md b/README.md index a1e3f51b..21e87431 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,38 @@ # Slothscript -A slow scripting language. +A scripting language for automation and system administration. ```sloth -func fib(n) { - if n <= 1 { return n } - fib(n - 1) + fib(n - 2) +# Single quotes delimit raw string literals, not supporting escapes or +# interpolation +let cxx = 'clang++' +let build_mode = 'Debug' +let generator = 'Ninja' + +# $scriptDir is set by the runtime to be the directory containing the +# currently running script (not necessarily the working directory). +# +# Double quotes delimit rich string literals, `${ expression }` is used for string +# interpolation +let build = Directory("${$scriptDir}/build") + +if not build.exists() { + build.create() } -print(fib(20)) +# Within this block, set the current working directory to be our build +# directory +with ($cwd = build.path) { + # The `!` postfix operator means spawn a sub-process, using the expression + # preceding it as a command and argument list, then block until the process + # exits + [ + 'cmake', $scriptDir, + "-DCMAKE_CXX_COMPILER=${cxx}", + "-DCMAKE_BUILD_TYPE=${build_mode}", + '-G', generator, + ]! +} ``` ## Installation @@ -70,6 +94,7 @@ print(counter()) - `false` - `Bool` literal - `null` - `Null` literal singleton - `not` - prefix logical NOT operator; `assert(not false)` +- `throw` - raise an exception ### Context Variables diff --git a/README.tmpl.md b/README.tmpl.md index baa7f502..1c2c76d0 100644 --- a/README.tmpl.md +++ b/README.tmpl.md @@ -1,9 +1,9 @@ # Slothscript -A slow scripting language. +A scripting language for automation and system administration. ```sloth -{{ .test_green_specs_fibonacci_sloth }} +{{ .test_green_specs_configure_cmake_sloth }} ``` ## Installation @@ -53,6 +53,7 @@ First-class functions: - `false` - `Bool` literal - `null` - `Null` literal singleton - `not` - prefix logical NOT operator; `assert(not false)` +- `throw` - raise an exception ### Context Variables diff --git a/dbc.json b/dbc.json index 9218234c..a79b7572 100644 --- a/dbc.json +++ b/dbc.json @@ -4,14 +4,14 @@ "output": "README.md", "template": "README.tmpl.md", "inputs": [ - "test/green_specs/fibonacci.sloth", + "test/green_specs/configure_cmake.sloth", "test/green_specs/var_reference.sloth", "test/green_specs/first_class_func.sloth", "docs/context.md" ] }, { - "output": "test/green_specs/fibonacci.sloth", + "output": "test/green_specs/configure_cmake.sloth", "filter": "awk '/### Program/{f=1; next} /### /{f=0} f'" }, { diff --git a/dune-project b/dune-project index 37495e80..5a8c72a0 100644 --- a/dune-project +++ b/dune-project @@ -8,9 +8,9 @@ (source (github christopherfujino/slothscript)) -(authors "Christopher Fujino") +(authors "Christopher Fujino ") -(maintainers "Christopher Fujino") +(maintainers "Christopher Fujino ") (license "BSD-3-Clause") diff --git a/lib/interpreter/native.ml b/lib/interpreter/native.ml index fc91b1bf..fc5b3985 100644 --- a/lib/interpreter/native.ml +++ b/lib/interpreter/native.ml @@ -620,8 +620,8 @@ module Make_test () : TestSig = struct | _ -> let msg = Printf.sprintf "Tried to execute sub-process %s but expected %s" - (List.to_string ~f:Fun.id hd.cmd) (List.to_string ~f:Fun.id proc.cmd) + (List.to_string ~f:Fun.id hd.cmd) in Error msg) in diff --git a/native/cpp/bytecode/.clangd b/native/cpp/bytecode/.clangd new file mode 100644 index 00000000..01cd9a95 --- /dev/null +++ b/native/cpp/bytecode/.clangd @@ -0,0 +1,4 @@ +CompileFlags: + # Un-comment this when we start building from Dune + #CompilationDatabase: ../.. + CompilationDatabase: build/ diff --git a/native/cpp/bytecode/CMakeLists.txt b/native/cpp/bytecode/CMakeLists.txt new file mode 100644 index 00000000..f7078371 --- /dev/null +++ b/native/cpp/bytecode/CMakeLists.txt @@ -0,0 +1,38 @@ +## Global + +cmake_minimum_required(VERSION 3.31) +project(slothscript-cpp-bytecode VERSION 0.1) + +# These CMAKE_* variables apply to all future targets +set(CMAKE_CXX_EXTENSIONS OFF) # why don't want GNU extension +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Generate a compile_commands.json file +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# IWYU +find_program(IWYU_PATH NAMES include-what-you-use iwyu) +if(NOT IWYU_PATH) + message(FATAL_ERROR + "Could not find the program include-what-you-use (apt install iwyu)") +endif() +set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE ${IWYU_PATH}) + +add_compile_options(-Wall -Werror -Wpedantic -Wextra) + +# https://stackoverflow.com/questions/24648357/compiling-a-static-executable-with-cmake +set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") +set(BUILD_SHARED_LIBS OFF) +set(CMAKE_EXE_LINKER_FLAGS "-static") + +## Test exe + +add_executable(test test.cpp) + +target_link_libraries(test bytecode) +target_include_directories(test PRIVATE ../../../ignore/test.hpp) + +## Bytecode library + +add_library(bytecode STATIC bytecode.cpp) diff --git a/native/cpp/bytecode/bytecode.cpp b/native/cpp/bytecode/bytecode.cpp new file mode 100644 index 00000000..88f1e788 --- /dev/null +++ b/native/cpp/bytecode/bytecode.cpp @@ -0,0 +1,37 @@ +#include "bytecode.hpp" + +#include +#include +#include + +std::string Chunk::debug() { + std::string message; + size_t byteSize = bytes.size(); + for (size_t offset = 0; offset < byteSize;) { + auto pair = _debugInstruction(offset); + message += pair.first; + offset = pair.second; + } + + return message; +} + +std::pair Chunk::_debugInstruction(size_t offset) { + std::string message = std::format("{: 4} ", offset); + + switch ((OpCode)bytes[offset]) { + case OpCode::RETURN: + message += "RETURN\n"; + return {message, offset + 1}; + } + throw std::runtime_error("Unreachable"); +} + +Chunk::Chunk(std::vector bytes) : bytes(bytes) {} + +VM::VM(std::vector chunks) : _chunks(chunks) { + for (Chunk &chunk : chunks) { + // TODO: implement + ignore(chunk); + } +} diff --git a/native/cpp/bytecode/bytecode.hpp b/native/cpp/bytecode/bytecode.hpp new file mode 100644 index 00000000..bfb8eb49 --- /dev/null +++ b/native/cpp/bytecode/bytecode.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include // for size_t +#include +#include // for pair +#include + +static_assert(sizeof(double) == 8, "double is not 64-bit"); +static_assert(std::numeric_limits::is_iec559, + "double is not IEEE 754 compliant"); + +typedef double Value; + +enum class OpCode : uint8_t { + RETURN, +}; + +class Chunk { +public: + Chunk(std::vector bytes); + + std::string debug(); + + std::vector bytes; + +private: + std::pair _debugInstruction(size_t offset); +}; + +template inline void ignore(T) {} + +class VM { +public: + VM(std::vector chunks); + +private: + std::vector _chunks; +}; diff --git a/native/cpp/bytecode/configure.sloth b/native/cpp/bytecode/configure.sloth new file mode 100755 index 00000000..0eb1167f --- /dev/null +++ b/native/cpp/bytecode/configure.sloth @@ -0,0 +1,31 @@ +#!/usr/bin/env sloth + +func ensureTestHpp() { + let testHppDir = Directory("${$scriptDir}/../../../ignore/test.hpp") + if not testHppDir.exists() { + let monorepoDir = Directory("${$scriptDir}/../../../../../cpp/test.hpp") + if monorepoDir.exists() { + # TODO make these realpaths + ['ln', '-s', monorepoDir.path, testHppDir.path]! + } else { + let remote = 'git@github.com:christopherfujino/test.hpp.git' + ['git', 'clone', '--depth=1', remote, testHppDir.path]! + } + } +} + +ensureTestHpp() + +let build = Directory("${$scriptDir}/build") +if not build.exists() { + build.create() +} + +with ($cwd = build.path) { + [ + 'cmake', '..', + '-GNinja', + '-DCMAKE_CXX_COMPILER=clang++', + ]! + 'cmake --build .'! +} diff --git a/native/cpp/bytecode/test.cpp b/native/cpp/bytecode/test.cpp new file mode 100644 index 00000000..7e2fe2f0 --- /dev/null +++ b/native/cpp/bytecode/test.cpp @@ -0,0 +1,19 @@ +#include "test.hpp" +#include "bytecode.hpp" +#include + +using namespace _CHRIS_MONOREPO_CPP_TEST; + +static std::vector tests = { + {"RETURN is 0", []() { expect((int)OpCode::RETURN, 0); }}, + {"Chunk is just a vector", + []() { expect((int)sizeof(Chunk), (int)sizeof(std::vector)); }}, + {"Create a RETURN chunk", + []() { + Chunk chunk{{(uint8_t)OpCode::RETURN}}; + expect((int)chunk.bytes.size(), 1); + expect(chunk.debug(), std::string(" 0 RETURN\n")); + }}, +}; + +int main() { return Runner(tests).run(); } diff --git a/native/cpp/bytecode/test.sh b/native/cpp/bytecode/test.sh new file mode 100755 index 00000000..3c583490 --- /dev/null +++ b/native/cpp/bytecode/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPTDIR="$( dirname "$( realpath "${BASH_SOURCE[0]}" )" )" + +BUILD="${SCRIPTDIR}/build" + +cmake --build "$BUILD" +"${BUILD}/test" diff --git a/native/dune b/native/dune index 0df2a9f6..4c546437 100644 --- a/native/dune +++ b/native/dune @@ -8,10 +8,10 @@ (data_only_dirs cpp) (rule + (targets libsloth_native.a compile_commands.json) (mode promote) (deps (source_tree cpp)) - (targets libsloth_native.a compile_commands.json) (action ; https://dune.readthedocs.io/en/stable/reference/actions/no-infer.html ; no infer because dune doesn't know how the targets are being created diff --git a/sloth_script.opam b/sloth_script.opam index 515680b3..ac82471f 100644 --- a/sloth_script.opam +++ b/sloth_script.opam @@ -2,8 +2,8 @@ opam-version: "2.0" synopsis: "A slow scripting language" description: "A longer description" -maintainer: ["Christopher Fujino"] -authors: ["Christopher Fujino"] +maintainer: ["Christopher Fujino "] +authors: ["Christopher Fujino "] license: "BSD-3-Clause" tags: ["topics" "to describe" "your" "project"] homepage: "https://github.com/christopherfujino/slothscript" diff --git a/test/green_specs/configure_cmake.sloth b/test/green_specs/configure_cmake.sloth new file mode 100644 index 00000000..96b633d6 --- /dev/null +++ b/test/green_specs/configure_cmake.sloth @@ -0,0 +1,197 @@ +### Program +# Single quotes delimit raw string literals, not supporting escapes or +# interpolation +let cxx = 'clang++' +let build_mode = 'Debug' +let generator = 'Ninja' + +# $scriptDir is set by the runtime to be the directory containing the +# currently running script (not necessarily the working directory). +# +# Double quotes delimit rich string literals, `${ expression }` is used for string +# interpolation +let build = Directory("${$scriptDir}/build") + +if not build.exists() { + build.create() +} + +# Within this block, set the current working directory to be our build +# directory +with ($cwd = build.path) { + # The `!` postfix operator means spawn a sub-process, using the expression + # preceding it as a command and argument list, then block until the process + # exits + [ + 'cmake', $scriptDir, + "-DCMAKE_CXX_COMPILER=${cxx}", + "-DCMAKE_BUILD_TYPE=${build_mode}", + '-G', generator, + ]! +} + +### Processes +(((cmd + (cmake + /parent + -DCMAKE_CXX_COMPILER=clang++ + -DCMAKE_BUILD_TYPE=Debug + -G + Ninja)) + (instructions + ()))) + +### Ast +((StmtDecl + (ExprStmt + (LetExpr + cxx + (String + ((FullString + clang++ + )) + ) + ))) + (StmtDecl + (ExprStmt + (LetExpr + build_mode + (String + ((FullString + Debug + )) + ) + ))) + (StmtDecl + (ExprStmt + (LetExpr + generator + (String + ((FullString + Ninja + )) + ) + ))) + (StmtDecl + (ExprStmt + (LetExpr + build + (FuncInvoc + (IdRef + Directory + ) + ((String + ((StartStringInterp + "" + ) + (ExpressionStringInterp + (ContextId + $scriptDir + )) + (EndStringInterp + /build + )) + )) + ) + ))) + (StmtDecl + (ExprStmt + (IfExpr + (IfCont + (conditional + (UnaryExpr + (target + (FuncInvoc + (ObjDeref + (IdRef + build + ) + exists + ) + () + )) + (pos + ) + (operator + Not))) + (block + ((ExprStmt + (FuncInvoc + (ObjDeref + (IdRef + build + ) + create + ) + () + )))) + (continuation + ()) + (pos + )) + ))) + (StmtDecl + (ExprStmt + (WithExpr + (($cwd + (ObjDeref + (IdRef + build + ) + path + ))) + ((ExprStmt + (UnaryExpr + (target + (List + ((String + ((FullString + cmake + )) + ) + (ContextId + $scriptDir + ) + (String + ((StartStringInterp + -DCMAKE_CXX_COMPILER= + ) + (ExpressionStringInterp + (IdRef + cxx + )) + (EndStringInterp + "" + )) + ) + (String + ((StartStringInterp + -DCMAKE_BUILD_TYPE= + ) + (ExpressionStringInterp + (IdRef + build_mode + )) + (EndStringInterp + "" + )) + ) + (String + ((FullString + -G + )) + ) + (IdRef + generator + )) + )) + (pos + ) + (operator + Bang)))) + )))) + +### Stdout + + +### Failure