diff options
Diffstat (limited to 'bytecode')
| -rw-r--r-- | bytecode/Cargo.nix | 742 | ||||
| -rw-r--r-- | bytecode/src/bytecode.rs | 721 | ||||
| -rw-r--r-- | bytecode/src/collector.rs | 1 | ||||
| -rw-r--r-- | bytecode/src/data.rs | 115 | ||||
| -rw-r--r-- | bytecode/src/encoding.rs | 279 | ||||
| -rw-r--r-- | bytecode/src/heap.rs | 68 | ||||
| -rw-r--r-- | bytecode/src/main.rs | 6 | ||||
| -rw-r--r-- | bytecode/src/stack.rs | 33 |
8 files changed, 1169 insertions, 796 deletions
diff --git a/bytecode/Cargo.nix b/bytecode/Cargo.nix new file mode 100644 index 0000000..136e315 --- /dev/null +++ b/bytecode/Cargo.nix @@ -0,0 +1,742 @@ + +# This file was @generated by crate2nix 0.10.0 with the command: +# "generate" +# See https://github.com/kolloch/crate2nix for more info. + +{ nixpkgs ? <nixpkgs> +, pkgs ? import nixpkgs { config = {}; } +, lib ? pkgs.lib +, stdenv ? pkgs.stdenv +, buildRustCrateForPkgs ? if buildRustCrate != null + then lib.warn "crate2nix: Passing `buildRustCrate` as argument to Cargo.nix is deprecated. If you don't customize `buildRustCrate`, replace `callPackage ./Cargo.nix {}` by `import ./Cargo.nix { inherit pkgs; }`, and if you need to customize `buildRustCrate`, use `buildRustCrateForPkgs` instead." (_: buildRustCrate) + else pkgs: pkgs.buildRustCrate + # Deprecated +, buildRustCrate ? null + # This is used as the `crateOverrides` argument for `buildRustCrate`. +, defaultCrateOverrides ? pkgs.defaultCrateOverrides + # The features to enable for the root_crate or the workspace_members. +, rootFeatures ? [ "default" ] + # If true, throw errors instead of issueing deprecation warnings. +, strictDeprecation ? false + # Used for conditional compilation based on CPU feature detection. +, targetFeatures ? [] + # Whether to perform release builds: longer compile times, faster binaries. +, release ? true + # Additional crate2nix configuration if it exists. +, crateConfig + ? if builtins.pathExists ./crate-config.nix + then pkgs.callPackage ./crate-config.nix {} + else {} +}: + +rec { + # + # "public" attributes that we attempt to keep stable with new versions of crate2nix. + # + + rootCrate = rec { + packageId = "bytecode"; + + # Use this attribute to refer to the derivation building your root crate package. + # You can override the features with rootCrate.build.override { features = [ "default" "feature1" ... ]; }. + build = internal.buildRustCrateWithFeatures { + inherit packageId; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; + # Refer your crate build derivation by name here. + # You can override the features with + # workspaceMembers."${crateName}".build.override { features = [ "default" "feature1" ... ]; }. + workspaceMembers = { + "bytecode" = rec { + packageId = "bytecode"; + build = internal.buildRustCrateWithFeatures { + packageId = "bytecode"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; + }; + + # A derivation that joins the outputs of all workspace members together. + allWorkspaceMembers = pkgs.symlinkJoin { + name = "all-workspace-members"; + paths = + let members = builtins.attrValues workspaceMembers; + in builtins.map (m: m.build) members; + }; + + # + # "internal" ("private") attributes that may change in every new version of crate2nix. + # + + internal = rec { + # Build and dependency information for crates. + # Many of the fields are passed one-to-one to buildRustCrate. + # + # Noteworthy: + # * `dependencies`/`buildDependencies`: similar to the corresponding fields for buildRustCrate. + # but with additional information which is used during dependency/feature resolution. + # * `resolvedDependencies`: the selected default features reported by cargo - only included for debugging. + # * `devDependencies` as of now not used by `buildRustCrate` but used to + # inject test dependencies into the build + + crates = { + "bytecode" = rec { + crateName = "bytecode"; + version = "0.1.0"; + edition = "2021"; + crateBin = [ + { name = "bytecode"; path = "src/main.rs"; } + ]; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./.; }; + + }; + }; + + # +# crate2nix/default.nix (excerpt start) +# + + /* Target (platform) data for conditional dependencies. + This corresponds roughly to what buildRustCrate is setting. + */ + defaultTarget = { + unix = true; + windows = false; + fuchsia = true; + test = false; + + # This doesn't appear to be officially documented anywhere yet. + # See https://github.com/rust-lang-nursery/rust-forge/issues/101. + os = + if stdenv.hostPlatform.isDarwin + then "macos" + else stdenv.hostPlatform.parsed.kernel.name; + arch = stdenv.hostPlatform.parsed.cpu.name; + family = "unix"; + env = "gnu"; + endian = + if stdenv.hostPlatform.parsed.cpu.significantByte.name == "littleEndian" + then "little" else "big"; + pointer_width = toString stdenv.hostPlatform.parsed.cpu.bits; + vendor = stdenv.hostPlatform.parsed.vendor.name; + debug_assertions = false; + }; + + /* Filters common temp files and build files. */ + # TODO(pkolloch): Substitute with gitignore filter + sourceFilter = name: type: + let + baseName = builtins.baseNameOf (builtins.toString name); + in + ! ( + # Filter out git + baseName == ".gitignore" + || (type == "directory" && baseName == ".git") + + # Filter out build results + || ( + type == "directory" && ( + baseName == "target" + || baseName == "_site" + || baseName == ".sass-cache" + || baseName == ".jekyll-metadata" + || baseName == "build-artifacts" + ) + ) + + # Filter out nix-build result symlinks + || ( + type == "symlink" && lib.hasPrefix "result" baseName + ) + + # Filter out IDE config + || ( + type == "directory" && ( + baseName == ".idea" || baseName == ".vscode" + ) + ) || lib.hasSuffix ".iml" baseName + + # Filter out nix build files + || baseName == "Cargo.nix" + + # Filter out editor backup / swap files. + || lib.hasSuffix "~" baseName + || builtins.match "^\\.sw[a-z]$$" baseName != null + || builtins.match "^\\..*\\.sw[a-z]$$" baseName != null + || lib.hasSuffix ".tmp" baseName + || lib.hasSuffix ".bak" baseName + || baseName == "tests.nix" + ); + + /* Returns a crate which depends on successful test execution + of crate given as the second argument. + + testCrateFlags: list of flags to pass to the test exectuable + testInputs: list of packages that should be available during test execution + */ + crateWithTest = { crate, testCrate, testCrateFlags, testInputs, testPreRun, testPostRun }: + assert builtins.typeOf testCrateFlags == "list"; + assert builtins.typeOf testInputs == "list"; + assert builtins.typeOf testPreRun == "string"; + assert builtins.typeOf testPostRun == "string"; + let + # override the `crate` so that it will build and execute tests instead of + # building the actual lib and bin targets We just have to pass `--test` + # to rustc and it will do the right thing. We execute the tests and copy + # their log and the test executables to $out for later inspection. + test = + let + drv = testCrate.override + ( + _: { + buildTests = true; + } + ); + # If the user hasn't set any pre/post commands, we don't want to + # insert empty lines. This means that any existing users of crate2nix + # don't get a spurious rebuild unless they set these explicitly. + testCommand = pkgs.lib.concatStringsSep "\n" + (pkgs.lib.filter (s: s != "") [ + testPreRun + "$f $testCrateFlags 2>&1 | tee -a $out" + testPostRun + ]); + in + pkgs.runCommand "run-tests-${testCrate.name}" + { + inherit testCrateFlags; + buildInputs = testInputs; + } '' + set -ex + + export RUST_BACKTRACE=1 + + # recreate a file hierarchy as when running tests with cargo + + # the source for test data + ${pkgs.xorg.lndir}/bin/lndir ${crate.src} + + # build outputs + testRoot=target/debug + mkdir -p $testRoot + + # executables of the crate + # we copy to prevent std::env::current_exe() to resolve to a store location + for i in ${crate}/bin/*; do + cp "$i" "$testRoot" + done + chmod +w -R . + + # test harness executables are suffixed with a hash, like cargo does + # this allows to prevent name collision with the main + # executables of the crate + hash=$(basename $out) + for file in ${drv}/tests/*; do + f=$testRoot/$(basename $file)-$hash + cp $file $f + ${testCommand} + done + ''; + in + pkgs.runCommand "${crate.name}-linked" + { + inherit (crate) outputs crateName; + passthru = (crate.passthru or { }) // { + inherit test; + }; + } '' + echo tested by ${test} + ${lib.concatMapStringsSep "\n" (output: "ln -s ${crate.${output}} ${"$"}${output}") crate.outputs} + ''; + + /* A restricted overridable version of builtRustCratesWithFeatures. */ + buildRustCrateWithFeatures = + { packageId + , features ? rootFeatures + , crateOverrides ? defaultCrateOverrides + , buildRustCrateForPkgsFunc ? null + , runTests ? false + , testCrateFlags ? [ ] + , testInputs ? [ ] + # Any command to run immediatelly before a test is executed. + , testPreRun ? "" + # Any command run immediatelly after a test is executed. + , testPostRun ? "" + }: + lib.makeOverridable + ( + { features + , crateOverrides + , runTests + , testCrateFlags + , testInputs + , testPreRun + , testPostRun + }: + let + buildRustCrateForPkgsFuncOverriden = + if buildRustCrateForPkgsFunc != null + then buildRustCrateForPkgsFunc + else + ( + if crateOverrides == pkgs.defaultCrateOverrides + then buildRustCrateForPkgs + else + pkgs: (buildRustCrateForPkgs pkgs).override { + defaultCrateOverrides = crateOverrides; + } + ); + builtRustCrates = builtRustCratesWithFeatures { + inherit packageId features; + buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden; + runTests = false; + }; + builtTestRustCrates = builtRustCratesWithFeatures { + inherit packageId features; + buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden; + runTests = true; + }; + drv = builtRustCrates.crates.${packageId}; + testDrv = builtTestRustCrates.crates.${packageId}; + derivation = + if runTests then + crateWithTest + { + crate = drv; + testCrate = testDrv; + inherit testCrateFlags testInputs testPreRun testPostRun; + } + else drv; + in + derivation + ) + { inherit features crateOverrides runTests testCrateFlags testInputs testPreRun testPostRun; }; + + /* Returns an attr set with packageId mapped to the result of buildRustCrateForPkgsFunc + for the corresponding crate. + */ + builtRustCratesWithFeatures = + { packageId + , features + , crateConfigs ? crates + , buildRustCrateForPkgsFunc + , runTests + , target ? defaultTarget + } @ args: + assert (builtins.isAttrs crateConfigs); + assert (builtins.isString packageId); + assert (builtins.isList features); + assert (builtins.isAttrs target); + assert (builtins.isBool runTests); + let + rootPackageId = packageId; + mergedFeatures = mergePackageFeatures + ( + args // { + inherit rootPackageId; + target = target // { test = runTests; }; + } + ); + # Memoize built packages so that reappearing packages are only built once. + builtByPackageIdByPkgs = mkBuiltByPackageIdByPkgs pkgs; + mkBuiltByPackageIdByPkgs = pkgs: + let + self = { + crates = lib.mapAttrs (packageId: value: buildByPackageIdForPkgsImpl self pkgs packageId) crateConfigs; + build = mkBuiltByPackageIdByPkgs pkgs.buildPackages; + }; + in + self; + buildByPackageIdForPkgsImpl = self: pkgs: packageId: + let + features = mergedFeatures."${packageId}" or [ ]; + crateConfig' = crateConfigs."${packageId}"; + crateConfig = + builtins.removeAttrs crateConfig' [ "resolvedDefaultFeatures" "devDependencies" ]; + devDependencies = + lib.optionals + (runTests && packageId == rootPackageId) + (crateConfig'.devDependencies or [ ]); + dependencies = + dependencyDerivations { + inherit features target; + buildByPackageId = depPackageId: + # proc_macro crates must be compiled for the build architecture + if crateConfigs.${depPackageId}.procMacro or false + then self.build.crates.${depPackageId} + else self.crates.${depPackageId}; + dependencies = + (crateConfig.dependencies or [ ]) + ++ devDependencies; + }; + buildDependencies = + dependencyDerivations { + inherit features target; + buildByPackageId = depPackageId: + self.build.crates.${depPackageId}; + dependencies = crateConfig.buildDependencies or [ ]; + }; + filterEnabledDependenciesForThis = dependencies: filterEnabledDependencies { + inherit dependencies features target; + }; + dependenciesWithRenames = + lib.filter (d: d ? "rename") + ( + filterEnabledDependenciesForThis + ( + (crateConfig.buildDependencies or [ ]) + ++ (crateConfig.dependencies or [ ]) + ++ devDependencies + ) + ); + # Crate renames have the form: + # + # { + # crate_name = [ + # { version = "1.2.3"; rename = "crate_name01"; } + # ]; + # # ... + # } + crateRenames = + let + grouped = + lib.groupBy + (dependency: dependency.name) + dependenciesWithRenames; + versionAndRename = dep: + let + package = crateConfigs."${dep.packageId}"; + in + { inherit (dep) rename; version = package.version; }; + in + lib.mapAttrs (name: choices: builtins.map versionAndRename choices) grouped; + in + buildRustCrateForPkgsFunc pkgs + ( + crateConfig // { + src = crateConfig.src or ( + pkgs.fetchurl rec { + name = "${crateConfig.crateName}-${crateConfig.version}.tar.gz"; + # https://www.pietroalbini.org/blog/downloading-crates-io/ + # Not rate-limited, CDN URL. + url = "https://static.crates.io/crates/${crateConfig.crateName}/${crateConfig.crateName}-${crateConfig.version}.crate"; + sha256 = + assert (lib.assertMsg (crateConfig ? sha256) "Missing sha256 for ${name}"); + crateConfig.sha256; + } + ); + extraRustcOpts = lib.lists.optional (targetFeatures != [ ]) "-C target-feature=${lib.concatMapStringsSep "," (x: "+${x}") targetFeatures}"; + inherit features dependencies buildDependencies crateRenames release; + } + ); + in + builtByPackageIdByPkgs; + + /* Returns the actual derivations for the given dependencies. */ + dependencyDerivations = + { buildByPackageId + , features + , dependencies + , target + }: + assert (builtins.isList features); + assert (builtins.isList dependencies); + assert (builtins.isAttrs target); + let + enabledDependencies = filterEnabledDependencies { + inherit dependencies features target; + }; + depDerivation = dependency: buildByPackageId dependency.packageId; + in + map depDerivation enabledDependencies; + + /* Returns a sanitized version of val with all values substituted that cannot + be serialized as JSON. + */ + sanitizeForJson = val: + if builtins.isAttrs val + then lib.mapAttrs (n: v: sanitizeForJson v) val + else if builtins.isList val + then builtins.map sanitizeForJson val + else if builtins.isFunction val + then "function" + else val; + + /* Returns various tools to debug a crate. */ + debugCrate = { packageId, target ? defaultTarget }: + assert (builtins.isString packageId); + let + debug = rec { + # The built tree as passed to buildRustCrate. + buildTree = buildRustCrateWithFeatures { + buildRustCrateForPkgsFunc = _: lib.id; + inherit packageId; + }; + sanitizedBuildTree = sanitizeForJson buildTree; + dependencyTree = sanitizeForJson + ( + buildRustCrateWithFeatures { + buildRustCrateForPkgsFunc = _: crate: { + "01_crateName" = crate.crateName or false; + "02_features" = crate.features or [ ]; + "03_dependencies" = crate.dependencies or [ ]; + }; + inherit packageId; + } + ); + mergedPackageFeatures = mergePackageFeatures { + features = rootFeatures; + inherit packageId target; + }; + diffedDefaultPackageFeatures = diffDefaultPackageFeatures { + inherit packageId target; + }; + }; + in + { internal = debug; }; + + /* Returns differences between cargo default features and crate2nix default + features. + + This is useful for verifying the feature resolution in crate2nix. + */ + diffDefaultPackageFeatures = + { crateConfigs ? crates + , packageId + , target + }: + assert (builtins.isAttrs crateConfigs); + let + prefixValues = prefix: lib.mapAttrs (n: v: { "${prefix}" = v; }); + mergedFeatures = + prefixValues + "crate2nix" + (mergePackageFeatures { inherit crateConfigs packageId target; features = [ "default" ]; }); + configs = prefixValues "cargo" crateConfigs; + combined = lib.foldAttrs (a: b: a // b) { } [ mergedFeatures configs ]; + onlyInCargo = + builtins.attrNames + (lib.filterAttrs (n: v: !(v ? "crate2nix") && (v ? "cargo")) combined); + onlyInCrate2Nix = + builtins.attrNames + (lib.filterAttrs (n: v: (v ? "crate2nix") && !(v ? "cargo")) combined); + differentFeatures = lib.filterAttrs + ( + n: v: + (v ? "crate2nix") + && (v ? "cargo") + && (v.crate2nix.features or [ ]) != (v."cargo".resolved_default_features or [ ]) + ) + combined; + in + builtins.toJSON { + inherit onlyInCargo onlyInCrate2Nix differentFeatures; + }; + + /* Returns an attrset mapping packageId to the list of enabled features. + + If multiple paths to a dependency enable different features, the + corresponding feature sets are merged. Features in rust are additive. + */ + mergePackageFeatures = + { crateConfigs ? crates + , packageId + , rootPackageId ? packageId + , features ? rootFeatures + , dependencyPath ? [ crates.${packageId}.crateName ] + , featuresByPackageId ? { } + , target + # Adds devDependencies to the crate with rootPackageId. + , runTests ? false + , ... + } @ args: + assert (builtins.isAttrs crateConfigs); + assert (builtins.isString packageId); + assert (builtins.isString rootPackageId); + assert (builtins.isList features); + assert (builtins.isList dependencyPath); + assert (builtins.isAttrs featuresByPackageId); + assert (builtins.isAttrs target); + assert (builtins.isBool runTests); + let + crateConfig = crateConfigs."${packageId}" or (builtins.throw "Package not found: ${packageId}"); + expandedFeatures = expandFeatures (crateConfig.features or { }) features; + enabledFeatures = enableFeatures (crateConfig.dependencies or [ ]) expandedFeatures; + depWithResolvedFeatures = dependency: + let + packageId = dependency.packageId; + features = dependencyFeatures enabledFeatures dependency; + in + { inherit packageId features; }; + resolveDependencies = cache: path: dependencies: + assert (builtins.isAttrs cache); + assert (builtins.isList dependencies); + let + enabledDependencies = filterEnabledDependencies { + inherit dependencies target; + features = enabledFeatures; + }; + directDependencies = map depWithResolvedFeatures enabledDependencies; + foldOverCache = op: lib.foldl op cache directDependencies; + in + foldOverCache + ( + cache: { packageId, features }: + let + cacheFeatures = cache.${packageId} or [ ]; + combinedFeatures = sortedUnique (cacheFeatures ++ features); + in + if cache ? ${packageId} && cache.${packageId} == combinedFeatures + then cache + else + mergePackageFeatures { + features = combinedFeatures; + featuresByPackageId = cache; + inherit crateConfigs packageId target runTests rootPackageId; + } + ); + cacheWithSelf = + let + cacheFeatures = featuresByPackageId.${packageId} or [ ]; + combinedFeatures = sortedUnique (cacheFeatures ++ enabledFeatures); + in + featuresByPackageId // { + "${packageId}" = combinedFeatures; + }; + cacheWithDependencies = + resolveDependencies cacheWithSelf "dep" + ( + crateConfig.dependencies or [ ] + ++ lib.optionals + (runTests && packageId == rootPackageId) + (crateConfig.devDependencies or [ ]) + ); + cacheWithAll = + resolveDependencies + cacheWithDependencies "build" + (crateConfig.buildDependencies or [ ]); + in + cacheWithAll; + + /* Returns the enabled dependencies given the enabled features. */ + filterEnabledDependencies = { dependencies, features, target }: + assert (builtins.isList dependencies); + assert (builtins.isList features); + assert (builtins.isAttrs target); + + lib.filter + ( + dep: + let + targetFunc = dep.target or (features: true); + in + targetFunc { inherit features target; } + && ( + !(dep.optional or false) + || builtins.any (doesFeatureEnableDependency dep) features + ) + ) + dependencies; + + /* Returns whether the given feature should enable the given dependency. */ + doesFeatureEnableDependency = { name, rename ? null, ... }: feature: + let + prefix = "${name}/"; + len = builtins.stringLength prefix; + startsWithPrefix = builtins.substring 0 len feature == prefix; + in + (rename == null && feature == name) + || (rename != null && rename == feature) + || startsWithPrefix; + + /* Returns the expanded features for the given inputFeatures by applying the + rules in featureMap. + + featureMap is an attribute set which maps feature names to lists of further + feature names to enable in case this feature is selected. + */ + expandFeatures = featureMap: inputFeatures: + assert (builtins.isAttrs featureMap); + assert (builtins.isList inputFeatures); + let + expandFeature = feature: + assert (builtins.isString feature); + [ feature ] ++ (expandFeatures featureMap (featureMap."${feature}" or [ ])); + outFeatures = lib.concatMap expandFeature inputFeatures; + in + sortedUnique outFeatures; + + /* This function adds optional dependencies as features if they are enabled + indirectly by dependency features. This function mimics Cargo's behavior + described in a note at: + https://doc.rust-lang.org/nightly/cargo/reference/features.html#dependency-features + */ + enableFeatures = dependencies: features: + assert (builtins.isList features); + assert (builtins.isList dependencies); + let + additionalFeatures = lib.concatMap + ( + dependency: + assert (builtins.isAttrs dependency); + let + enabled = builtins.any (doesFeatureEnableDependency dependency) features; + in + if (dependency.optional or false) && enabled then [ dependency.name ] else [ ] + ) + dependencies; + in + sortedUnique (features ++ additionalFeatures); + + /* + Returns the actual features for the given dependency. + + features: The features of the crate that refers this dependency. + */ + dependencyFeatures = features: dependency: + assert (builtins.isList features); + assert (builtins.isAttrs dependency); + let + defaultOrNil = + if dependency.usesDefaultFeatures or true + then [ "default" ] + else [ ]; + explicitFeatures = dependency.features or [ ]; + additionalDependencyFeatures = + let + dependencyPrefix = (dependency.rename or dependency.name) + "/"; + dependencyFeatures = + builtins.filter (f: lib.hasPrefix dependencyPrefix f) features; + in + builtins.map (lib.removePrefix dependencyPrefix) dependencyFeatures; + in + defaultOrNil ++ explicitFeatures ++ additionalDependencyFeatures; + + /* Sorts and removes duplicates from a list of strings. */ + sortedUnique = features: + assert (builtins.isList features); + assert (builtins.all builtins.isString features); + let + outFeaturesSet = lib.foldl (set: feature: set // { "${feature}" = 1; }) { } features; + outFeaturesUnique = builtins.attrNames outFeaturesSet; + in + builtins.sort (a: b: a < b) outFeaturesUnique; + + deprecationWarning = message: value: + if strictDeprecation + then builtins.throw "strictDeprecation enabled, aborting: ${message}" + else builtins.trace message value; + + # + # crate2nix/default.nix (excerpt end) + # + }; +} + diff --git a/bytecode/src/bytecode.rs b/bytecode/src/bytecode.rs index 84a9b8b..df07485 100644 --- a/bytecode/src/bytecode.rs +++ b/bytecode/src/bytecode.rs @@ -1,598 +1,429 @@ -use crate::data::{Pointer, Value}; +use crate::data::Value; use crate::heap::Heap; -use std::io::{BufWriter, Read, StdinLock, Write}; -// The cute scheme virtual machine has two stacks: -// - the data stack and -// - the locals stack. -// There are four pointers: -// - the instruction pointer, -// - the argument pointer, -// - and the (data) stack pointer. -// These pointers cannot be manipulated directly, but are referred to in -// the comments below on the opcodes. +// The cute scheme virtual machine is a register based VM. +// There are 256 registers, also called locals. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct Local(pub u8); + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Arg { + L(Local), + Const(Value), +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum Op { // Signed arithmetic // ================= - // ( -- n ) - // Pushes a constant. - Const(i64), - // ( n1 n2 -- n3 ) // Adds two integers. - Add, - // ( n1 n2 -- n3 ) + Add(Local, Arg, Arg), // Subtracts two integers. - Sub, - // ( n1 n2 -- n3 ) + Sub(Local, Arg, Arg), // Multiplies two integers. - Mul, - // ( n1 n2 -- n3 ) + Mul(Local, Arg, Arg), // Divides two integers and truncates the result. - Div, - // ( n1 n2 -- n3 ) + Div(Local, Arg, Arg), // Remainder from Div. - Mod, + Mod(Local, Arg, Arg), // Heap // ====== - // ( n -- a ) // Allocates n words and returns the address. - Alloc, - // ( n -- a ) + Alloc(Local, Arg), // Allocates n bytes and returns the address. The contents of this allocation will be treated // as raw data and not walked by the garbage collector. - AllocBytevector, - // ( a i -- u ) + AllocBytevector(Local, Arg), // Fetches a 64 bit word from the specified address plus the // given offset. - Peek, - // ( u a i ) + Peek(Local, Arg, Arg), // Stores a 64 bit word at the specified address plus the // given offset. - Poke, - // ( a i -- n ) + Poke(Arg, Arg, Arg), // Fetches a byte from the specified address plus the given offset. - PeekByte, - // ( n a i ) + PeekByte(Local, Arg, Arg), // Stores a byte at the specified address plus the given offset. - PokeByte, + PokeByte(Arg, Arg, Arg), - // Stack - // ===== + // Locals + // ====== - // ( n -- ) - // Deletes an element from the stack. - Pop, - // ( -- n ) - // Copies an argument from the locals stack to the data stack. The argument is an index above the argument pointer. - Local(u8), + // Loads a value into a register. + Mov(Local, Arg), // Control flow // ============ - // ( f -- ) - // Jumps to the specified offset if the argument is not false. - If(i64), - // ( i-addr a1 a2 ... an ) - // ( Locals stack: -- instruction-ptr a1 a2 ... an argument-ptr ) - // Pushes the instruction pointer, moves n arguments to the locals - // stack, pushes the old argument pointer, and jumps to the - // specified location. - Call(u8), - // ( Locals stack: instruction-ptr a1 a2 ... an argument-ptr ) - // Pops the argument pointer (effectively popping n more arguments which were added by Call), - // and then pops the instruction pointer. - Ret, - // ( n ) + // Jumps to the specified address. + Jmp(Arg), + // Jumps to the specified address if the argument is not false. Note: the first argument is the + // condition and the second argument is the address. + JmpIf(Arg, Arg), // Terminates the interpreter with the given status code. - Exit, - - // Input and output - // ================ - - // ( b file -- f ) - // Writes a byte to a file. Returns true on success, false on error. - PutC, - // ( file -- b ) - // Reads one byte from a file. Returns -1 on EOF, 0 on error. - GetC, + Exit(Arg), } -struct Stack { - v: Vec<Value>, +struct Interpreter { + locals: Vec<Value>, + heap: Heap, } -impl Stack { - fn pop(&mut self) -> Result<Value, String> { - return self.v.pop().ok_or(String::from("stack underflow")); - } - - fn pop_int(&mut self) -> Result<i64, String> { - return self.pop()?.to_int(); - } - - fn push_int(&mut self, i: i64) { - self.v.push(Value::from_int(i)); +impl Interpreter { + fn read_arg(&self, x: Arg) -> Value { + match x { + Arg::L(Local(i)) => self.locals[usize::from(i)], + Arg::Const(c) => c, + } } - fn pop_pointer(&mut self) -> Result<Pointer, String> { - return self.pop()?.to_pointer(); + fn set_arg(&mut self, dest: Local, val: Value) { + let Local(i) = dest; + self.locals[usize::from(i)] = val; } - fn push_pointer(&mut self, p: Pointer) { - self.v.push(Value::from_pointer(p)); + fn add(&mut self, dest: Local, x: Arg, y: Arg) -> Result<(), String> { + let n1 = self.read_arg(x).to_int()?; + let n2 = self.read_arg(y).to_int()?; + self.set_arg(dest, Value::from_int(n1.wrapping_add(n2))); + Ok(()) } - fn pop_usize(&mut self) -> Result<usize, String> { - let Value(stack_representation) = self.pop()?; - Ok(usize::try_from(stack_representation).unwrap() >> 3) + fn sub(&mut self, dest: Local, x: Arg, y: Arg) -> Result<(), String> { + let n1 = self.read_arg(x).to_int()?; + let n2 = self.read_arg(y).to_int()?; + self.set_arg(dest, Value::from_int(n1.wrapping_sub(n2))); + Ok(()) } - fn push_usize(&mut self, p: usize) { - // We're going to disguise p as a pointer by shifting. - if p >= 0x2000000000000000 - /* 2^61 */ - { - panic!("pointer overflow!"); - } - self.v.push(Value(u64::try_from(p).unwrap() << 3)); + fn mul(&mut self, dest: Local, x: Arg, y: Arg) -> Result<(), String> { + let n1 = self.read_arg(x).to_int()?; + let n2 = self.read_arg(y).to_int()?; + self.set_arg(dest, Value::from_int(n1.wrapping_mul(n2))); + Ok(()) } -} - -fn fn_const(stack: &mut Stack, n: i64) { - stack.push_int(n); -} - -fn add(stack: &mut Stack) -> Result<(), String> { - let n2 = stack.pop_int()?; - let n1 = stack.pop_int()?; - stack.push_int(n1.wrapping_add(n2)); - Ok(()) -} -fn sub(stack: &mut Stack) -> Result<(), String> { - let n2 = stack.pop_int()?; - let n1 = stack.pop_int()?; - stack.push_int(n1.wrapping_sub(n2)); - Ok(()) -} - -fn mul(stack: &mut Stack) -> Result<(), String> { - let n2 = stack.pop_int()?; - let n1 = stack.pop_int()?; - stack.push_int(n1.wrapping_mul(n2)); - Ok(()) -} - -fn div(stack: &mut Stack) -> Result<(), String> { - let n2 = stack.pop_int()?; - let n1 = stack.pop_int()?; - stack.push_int(n1.wrapping_div(n2)); - Ok(()) -} - -fn fn_mod(stack: &mut Stack) -> Result<(), String> { - let n2 = stack.pop_int()?; - let n1 = stack.pop_int()?; - stack.push_int(n1.wrapping_rem(n2)); - Ok(()) -} - -fn alloc(stack: &mut Stack, locals: &mut Stack, heap: &mut Heap) -> Result<(), String> { - let n = stack.pop_int()?; - if n < 0 { - return Err(String::from("tried to allocate negative memory")); + fn div(&mut self, dest: Local, x: Arg, y: Arg) -> Result<(), String> { + let n1 = self.read_arg(x).to_int()?; + let n2 = self.read_arg(y).to_int()?; + self.set_arg(dest, Value::from_int(n1.wrapping_div(n2))); + Ok(()) } - let p = heap.alloc(usize::try_from(n).unwrap(), &mut stack.v, &mut locals.v)?; - stack.push_pointer(p); - return Ok(()); -} -fn alloc_bytevector(stack: &mut Stack, locals: &mut Stack, heap: &mut Heap) -> Result<(), String> { - let n = stack.pop_int()?; - if n < 0 { - return Err(String::from("tried to allocate negative memory")); + fn fn_mod(&mut self, dest: Local, x: Arg, y: Arg) -> Result<(), String> { + let n1 = self.read_arg(x).to_int()?; + let n2 = self.read_arg(y).to_int()?; + self.set_arg(dest, Value::from_int(n1.wrapping_rem(n2))); + Ok(()) } - let p = heap.alloc_bytevector(usize::try_from(n).unwrap(), &mut stack.v, &mut locals.v)?; - stack.push_pointer(p); - return Ok(()); -} - -fn peek(stack: &mut Stack, heap: &Heap) -> Result<(), String> { - let i = stack.pop_int()?; - let a = stack.pop_pointer()?; - stack.v.push(heap.peek(a.offset(i))?); - Ok(()) -} - -fn poke(stack: &mut Stack, heap: &mut Heap) -> Result<(), String> { - let i = stack.pop_int()?; - let a = stack.pop_pointer()?; - let u = stack.pop()?; - heap.poke(u, a.offset(i))?; - Ok(()) -} - -fn peek_byte(stack: &mut Stack, heap: &Heap) -> Result<(), String> { - let i = stack.pop_int()?; - let a = stack.pop_pointer()?; - stack.push_int(i64::from(heap.peek_byte(a.offset(i)))); - Ok(()) -} - -fn poke_byte(stack: &mut Stack, heap: &mut Heap) -> Result<(), String> { - let i = stack.pop_int()?; - let a = stack.pop_pointer()?; - let n = stack.pop_int()?; - heap.poke_byte((n & 0xff).try_into().unwrap(), a.offset(i)); - Ok(()) -} - -fn pop(stack: &mut Stack) -> Result<(), String> { - stack.pop()?; - Ok(()) -} - -fn local(stack: &mut Stack, locals: &Stack, n: u8) -> Result<(), String> { - let i = locals - .v - .len() - .checked_sub(usize::from(n) + 1) - .ok_or("out of bounds")?; - stack.v.push(locals.v[i]); - Ok(()) -} -fn fn_if(stack: &mut Stack, ip: &mut usize, n: i64) -> Result<(), String> { - let f = stack.pop_int()?; - if f != 0 { - if n > 0 { - *ip = ip - .checked_add(n.try_into().unwrap()) - .ok_or("invalid offset")?; - } else { - *ip = ip - .checked_sub((-n).try_into().unwrap()) - .ok_or("invalid offset")?; + fn alloc(&mut self, dest: Local, size: Arg) -> Result<(), String> { + let n = self.read_arg(size).to_int()?; + if n < 0 { + return Err(String::from("tried to allocate negative memory")); } + let p = self + .heap + .alloc(usize::try_from(n).unwrap(), &mut self.locals)?; + self.set_arg(dest, Value::from_pointer(p)); + Ok(()) } - Ok(()) -} - -fn call(stack: &mut Stack, locals: &mut Stack, ip: &mut usize, n: u8) -> Result<(), String> { - // Push the instruction pointer. - locals.push_usize(*ip); - let old_ap = locals.v.len(); - // Move n arguments to the locals stack. - let a1_idx = stack.v.len() - usize::from(n); - for i in a1_idx..stack.v.len() { - locals.v.push(stack.v[i]); - } - stack.v.truncate(a1_idx); - // Push the old argument pointer. - locals.push_usize(old_ap); - // Jump to the specified location. - let i_addr = stack.pop_int()?; - let i_addr_usize = match usize::try_from(i_addr) { - Ok(x) => x, - Err(_) => { - return Err(String::from("invalid address")); - } - }; - // Subtract 1 because the interpreter will also increment the - // instruction pointer. - *ip = i_addr_usize - 1; - Ok(()) -} - -fn ret(locals: &mut Stack, ip: &mut usize) -> Result<(), String> { - let ap = locals.pop_usize()?; - locals.v.truncate(ap); - let return_address = locals.pop_usize()?; - *ip = return_address; - Ok(()) -} - -trait File: Write + Read {} -struct FileTable<'a> { - files: Vec<Box<dyn File + 'a>>, -} - -impl<'a> FileTable<'a> { - fn file(&mut self, f: i64) -> Result<&mut (dyn File + 'a), String> { - if !(0 <= f && usize::try_from(f).unwrap() < self.files.len()) { - return Err(String::from("invalid file")); + fn alloc_bytevector(&mut self, dest: Local, size: Arg) -> Result<(), String> { + let n = self.read_arg(size).to_int()?; + if n < 0 { + return Err(String::from("tried to allocate negative memory")); } - Ok(&mut *self.files[usize::try_from(f).unwrap()]) + let p = self + .heap + .alloc_bytevector(usize::try_from(n).unwrap(), &mut self.locals)?; + self.set_arg(dest, Value::from_pointer(p)); + Ok(()) } -} - -struct Stdin<'a>(StdinLock<'a>); -impl<'a> Write for Stdin<'a> { - fn write(&mut self, _: &[u8]) -> std::io::Result<usize> { - Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "can't write to stdin", - )) + fn peek(&mut self, dest: Local, ptr: Arg, offset: Arg) -> Result<(), String> { + let p = self.read_arg(ptr).to_pointer()?; + let o = self.read_arg(offset).to_int()?; + if o < 0 { + return Err(String::from("pointer offset can't be negative")); + } + let result = self.heap.peek(p.offset(usize::try_from(o).unwrap()))?; + self.set_arg(dest, result); + Ok(()) } - fn flush(&mut self) -> std::io::Result<()> { + fn poke(&mut self, word: Arg, ptr: Arg, offset: Arg) -> Result<(), String> { + let w = self.read_arg(word); + let p = self.read_arg(ptr).to_pointer()?; + let o = self.read_arg(offset).to_int()?; + if o < 0 { + return Err(String::from("pointer offset can't be negative")); + } + self.heap.poke(w, p.offset(usize::try_from(o).unwrap()))?; Ok(()) } -} -impl<'a> Read for Stdin<'a> { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { - let Stdin(ref mut handle) = self; - handle.read(buf) + fn peek_byte(&mut self, dest: Local, ptr: Arg, offset: Arg) -> Result<(), String> { + let p = self.read_arg(ptr).to_pointer()?; + let o = self.read_arg(offset).to_int()?; + if o < 0 { + return Err(String::from("pointer offset can't be negative")); + } + let result = self.heap.peek_byte(p.offset(usize::try_from(o).unwrap()))?; + self.set_arg(dest, Value::from_int(i64::from(result))); + Ok(()) } -} - -impl<'a> File for Stdin<'a> {} -struct Out<T>(T); - -impl<T: Write> Write for Out<T> { - fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { - let Out(ref mut handle) = self; - handle.write(buf) + fn poke_byte(&mut self, word: Arg, ptr: Arg, offset: Arg) -> Result<(), String> { + let w = self.read_arg(word).to_int()?; + let p = self.read_arg(ptr).to_pointer()?; + let o = self.read_arg(offset).to_int()?; + if o < 0 { + return Err(String::from("pointer offset can't be negative")); + } + if !(0 <= w && w <= 0xff) { + return Err(format!("value {} is not byte-sized", w)); + } + self.heap.poke_byte( + u8::try_from(w).unwrap(), + p.offset(usize::try_from(o).unwrap()), + )?; + Ok(()) } - fn flush(&mut self) -> std::io::Result<()> { - let Out(ref mut handle) = self; - handle.flush() + fn mov(&mut self, dest: Local, src: Arg) { + self.set_arg(dest, self.read_arg(src)); } -} -impl<T> Read for Out<T> { - fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> { - Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "invalid file for reading", - )) + fn jmp(&mut self, ip: &mut usize, addr: Arg) -> Result<(), String> { + let a = self.read_arg(addr).to_int()?; + if a < 0 { + return Err(String::from("can't jump to a negative address")); + } + *ip = usize::try_from(a).unwrap(); + Ok(()) } -} - -impl<T: Write> File for Out<T> {} -fn putc(stack: &mut Stack, files: &mut FileTable) -> Result<(), String> { - let file = stack.pop_int()?; - let byte = stack.pop_int()?; - match files - .file(file)? - .write(&vec![(byte & 0xff).try_into().unwrap()]) - { - Ok(_) => { - stack.push_int(-1); + fn jmp_if(&mut self, ip: &mut usize, cond: Arg, addr: Arg) -> Result<(), String> { + let c = self.read_arg(cond); + let a = self.read_arg(addr).to_int()?; + if a < 0 { + return Err(String::from("can't jump to a negative address")); } - Err(_) => { - stack.push_int(0); + if c != Value::from_bool(false) { + *ip = usize::try_from(a).unwrap(); } + Ok(()) } - Ok(()) -} -fn getc(stack: &mut Stack, files: &mut FileTable) -> Result<(), String> { - let file = stack.pop_int()?; - let mut buf = vec![0]; - match files.file(file)?.read(&mut buf) { - Ok(1) => { - stack.push_int(buf[0].into()); - } - Ok(0) => { - stack.push_int(-1); - } - _ => { - stack.push_int(0); + fn eval(&mut self, prog: &[Op]) -> Result<u8, String> { + let mut ip = 0; + loop { + if ip >= prog.len() { + return Err(String::from("invalid instruction pointer")); + } + let op = prog[ip]; + ip += 1; + match op { + Op::Add(dest, x, y) => self.add(dest, x, y)?, + Op::Sub(dest, x, y) => self.sub(dest, x, y)?, + Op::Mul(dest, x, y) => self.mul(dest, x, y)?, + Op::Div(dest, x, y) => self.div(dest, x, y)?, + Op::Mod(dest, x, y) => self.fn_mod(dest, x, y)?, + Op::Alloc(dest, size) => self.alloc(dest, size)?, + Op::AllocBytevector(dest, size) => self.alloc_bytevector(dest, size)?, + Op::Peek(dest, ptr, offset) => self.peek(dest, ptr, offset)?, + Op::Poke(word, ptr, offset) => self.poke(word, ptr, offset)?, + Op::PeekByte(dest, ptr, offset) => self.peek_byte(dest, ptr, offset)?, + Op::PokeByte(word, ptr, offset) => self.poke_byte(word, ptr, offset)?, + Op::Mov(dest, src) => self.mov(dest, src), + Op::Jmp(addr) => self.jmp(&mut ip, addr)?, + Op::JmpIf(cond, addr) => self.jmp_if(&mut ip, cond, addr)?, + Op::Exit(code) => { + let n = self.read_arg(code).to_int()?; + return Ok((n & 0xff) as u8); + } + } } } - Ok(()) } pub fn eval(prog: &[Op]) -> Result<u8, String> { - let mut stack = Stack { v: Vec::new() }; - let mut locals_stack = Stack { v: Vec::new() }; - let mut heap = Heap::new(); - let mut ip = 0; - let stdin = std::io::stdin(); - let mut files = FileTable { - files: vec![ - Box::new(Stdin(stdin.lock())), - Box::new(Out(BufWriter::new(std::io::stdout()))), - Box::new(Out(BufWriter::new(std::io::stderr()))), - ], + let mut interpreter = Interpreter { + locals: vec![Value(0); 256], + heap: Heap::new(), }; - loop { - if ip >= prog.len() { - return Err(String::from("invalid instruction pointer")); - } - match prog[ip] { - Op::Const(n) => fn_const(&mut stack, n), - Op::Add => add(&mut stack)?, - Op::Sub => sub(&mut stack)?, - Op::Mul => mul(&mut stack)?, - Op::Div => div(&mut stack)?, - Op::Mod => fn_mod(&mut stack)?, - Op::Alloc => alloc(&mut stack, &mut locals_stack, &mut heap)?, - Op::AllocBytevector => alloc_bytevector(&mut stack, &mut locals_stack, &mut heap)?, - Op::Peek => peek(&mut stack, &heap)?, - Op::Poke => poke(&mut stack, &mut heap)?, - Op::PeekByte => peek_byte(&mut stack, &heap)?, - Op::PokeByte => poke_byte(&mut stack, &mut heap)?, - Op::Pop => pop(&mut stack)?, - Op::Local(n) => local(&mut stack, &locals_stack, n)?, - Op::If(n) => fn_if(&mut stack, &mut ip, n)?, - Op::Call(n) => call(&mut stack, &mut locals_stack, &mut ip, n)?, - Op::Ret => ret(&mut locals_stack, &mut ip)?, - Op::PutC => putc(&mut stack, &mut files)?, - Op::GetC => getc(&mut stack, &mut files)?, - Op::Exit => { - let n = stack.pop_int()?; - return Ok((n & 0xff) as u8); - } - } - ip += 1; - } + interpreter.eval(prog) } #[cfg(test)] mod tests { + use super::Arg::*; use super::Op::*; use super::*; #[test] fn eval_const() { - assert_eq!(Ok(5), eval(&vec![Const(5), Exit])); + assert_eq!(Ok(5), eval(&vec![Exit(Const(Value::from_int(5)))])); } #[test] fn eval_add() { - assert_eq!(Ok(10), eval(&vec![Const(5), Const(5), Add, Exit])); + assert_eq!( + Ok(10), + eval(&vec![ + Add( + Local(0), + Const(Value::from_int(5)), + Const(Value::from_int(5)) + ), + Exit(L(Local(0))), + ]) + ); } #[test] fn eval_sub() { - assert_eq!(Ok(2), eval(&vec![Const(5), Const(3), Sub, Exit])); + assert_eq!( + Ok(2), + eval(&vec![ + Sub( + Local(0), + Const(Value::from_int(5)), + Const(Value::from_int(3)) + ), + Exit(L(Local(0))), + ]) + ); } #[test] fn eval_mul() { - assert_eq!(Ok(25), eval(&vec![Const(5), Const(5), Mul, Exit])); - } - - #[test] - fn eval_div() { - assert_eq!(Ok(2), eval(&vec![Const(5), Const(2), Div, Exit])); - } - - #[test] - fn eval_mod() { - assert_eq!(Ok(1), eval(&vec![Const(5), Const(2), Mod, Exit])); - } - - #[test] - fn eval_alloc() { - assert_eq!(Ok(0), eval(&vec![Const(10), Alloc, Pop, Const(0), Exit])); - } - - #[test] - fn eval_bytevector() { assert_eq!( - Ok(0), - eval(&vec![Const(10), AllocBytevector, Pop, Const(0), Exit]) + Ok(25), + eval(&vec![ + Mul( + Local(0), + Const(Value::from_int(5)), + Const(Value::from_int(5)) + ), + Exit(L(Local(0))), + ]) ); } #[test] - fn eval_peek() { + fn eval_div() { assert_eq!( - Ok(0), - eval(&vec![Const(8), Alloc, Const(0), Peek, Const(0), Exit]) + Ok(2), + eval(&vec![ + Div( + Local(0), + Const(Value::from_int(5)), + Const(Value::from_int(2)) + ), + Exit(L(Local(0))), + ]) ); } #[test] - fn eval_poke() { + fn eval_mod() { assert_eq!( - Ok(0), + Ok(1), eval(&vec![ - Const(5), - Const(8), - Alloc, - Const(0), - Poke, - Const(0), - Exit + Mod( + Local(0), + Const(Value::from_int(5)), + Const(Value::from_int(2)) + ), + Exit(L(Local(0))), ]) ); } #[test] - fn eval_peek_byte() { + fn eval_alloc() { assert_eq!( Ok(0), - eval(&vec![Const(1), Alloc, Const(0), PeekByte, Exit]) + eval(&vec![ + Alloc(Local(0), Const(Value::from_int(10))), + Exit(Const(Value::from_int(0))), + ]) ); } #[test] - fn eval_poke_byte() { + fn eval_bytevector() { assert_eq!( Ok(0), eval(&vec![ - Const(5), - Const(1), - Alloc, - Const(0), - PokeByte, - Const(0), - Exit + AllocBytevector(Local(0), Const(Value::from_int(10))), + Exit(Const(Value::from_int(0))), ]) ); } #[test] - fn eval_pop() { - assert_eq!(Ok(5), eval(&vec![Const(5), Const(10), Pop, Exit])); - } - - #[test] - fn eval_if_true() { + fn eval_peek() { assert_eq!( Ok(5), - eval(&vec![Const(5), Const(1), If(2), Const(5), Add, Exit]) - ); - } - - #[test] - fn eval_if_false() { - assert_eq!( - Ok(10), - eval(&vec![Const(5), Const(0), If(2), Const(5), Add, Exit]) + eval(&vec![ + Alloc(Local(0), Const(Value::from_int(1))), + Poke( + Const(Value::from_int(5)), + L(Local(0)), + Const(Value::from_int(0)) + ), + Peek(Local(1), L(Local(0)), Const(Value::from_int(0))), + Exit(L(Local(1))), + ]) ); } #[test] - fn eval_call() { + fn eval_poke() { assert_eq!( - Ok(5), - eval(&vec![Const(3), Const(5), Call(1), Local(1), Exit]) + Ok(0), + eval(&vec![ + Alloc(Local(0), Const(Value::from_int(1))), + Poke( + Const(Value::from_int(5)), + L(Local(0)), + Const(Value::from_int(0)) + ), + Exit(Const(Value::from_int(0))), + ]) ); } #[test] - fn eval_ret() { + fn eval_peek_byte() { assert_eq!( - Ok(5), - eval(&vec![Const(4), Const(5), Call(1), Exit, Local(1), Ret]) + Ok(0), + eval(&vec![ + AllocBytevector(Local(0), Const(Value::from_int(1))), + PeekByte(Local(1), L(Local(0)), Const(Value::from_int(0))), + Exit(L(Local(1))), + ]) ); } #[test] - fn eval_putc() { + fn eval_poke_byte() { assert_eq!( - Ok(5), + Ok(0), eval(&vec![ - Const(5), - Const(88), - Const(1), - PutC, - If(2), - Const(5), - Add, - Exit + AllocBytevector(Local(0), Const(Value::from_int(1))), + PokeByte( + Const(Value::from_int(5)), + L(Local(0)), + Const(Value::from_int(0)) + ), + Exit(Const(Value::from_int(0))), ]) ); } - - #[test] - fn eval_getc_err() { - assert_eq!(Ok(0), eval(&vec![Const(1), GetC, Exit])); - } } diff --git a/bytecode/src/collector.rs b/bytecode/src/collector.rs deleted file mode 100644 index 38a2cf9..0000000 --- a/bytecode/src/collector.rs +++ /dev/null @@ -1 +0,0 @@ -fn collect_garbage(heap: diff --git a/bytecode/src/data.rs b/bytecode/src/data.rs index 4331f46..d6f9bd2 100644 --- a/bytecode/src/data.rs +++ b/bytecode/src/data.rs @@ -1,4 +1,7 @@ +use std::fmt; + // The data representations of values of different types are given below. +// - 0 represents an undefined value. Any operation with 0 will cause an error. // - Pointers are 64 bit unsigned (positive) ints which are // word-aligned, i.e. 0 mod 8. All types not listed below are // allocated on the heap behind a pointer. @@ -18,12 +21,17 @@ pub struct Pointer(pub usize); impl Pointer { - pub fn offset(self, i: i64) -> Self { + pub fn offset(self, i: usize) -> Self { let Pointer(u) = self; - if i > 0 { - return Pointer(u + usize::try_from(i).unwrap()); - } - return Pointer(u - usize::try_from(-i).unwrap()); + Pointer(u + i) + } +} + +impl fmt::LowerHex for Pointer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let val = self.0; + + fmt::LowerHex::fmt(&val, f) } } @@ -31,14 +39,9 @@ impl Pointer { pub struct Value(pub u64); impl Value { - pub fn is_nil(self) -> bool { - let Value(stack_representation) = self; - return stack_representation == 0; - } - pub fn is_pointer(self) -> bool { let Value(stack_representation) = self; - return stack_representation & 0x7 == 0; + return stack_representation != 0 && stack_representation & 0x7 == 0; } pub fn from_pointer(p: Pointer) -> Self { @@ -71,47 +74,12 @@ impl Value { return Ok(stack_representation as i64 >> 1); } - pub fn is_bool(self) -> bool { - let Value(stack_representation) = self; - return stack_representation == 0xa || stack_representation == 0x2; - } - pub fn from_bool(b: bool) -> Self { if b { return Value(0xa); } return Value(0x2); } - - pub fn to_bool(self) -> Result<bool, String> { - let Value(stack_representation) = self; - if !self.is_bool() { - return Err(format!("value {:x} is not a boolean", stack_representation)); - } - return Ok(stack_representation == 0xa); - } - - pub fn is_char(self) -> bool { - let Value(stack_representation) = self; - return stack_representation & 0x7 == 4; - } - - pub fn from_char(c: char) -> Self { - return Value(u64::from(c) << 32 | 0x4); - } - - pub fn to_char(self) -> Result<char, String> { - let Value(stack_representation) = self; - if !self.is_char() { - return Err(format!("value {:x} is not a char", stack_representation)); - } - return Ok( - char::from_u32((stack_representation >> 32) as u32).ok_or(format!( - "value {:x} is not a valid char", - stack_representation >> 32 - ))?, - ); - } } #[cfg(test)] @@ -119,16 +87,6 @@ mod tests { use super::*; #[test] - fn is_nil() { - assert_eq!(true, Value(0).is_nil()); - } - - #[test] - fn is_not_nil() { - assert_eq!(false, Value(1).is_nil()); - } - - #[test] fn is_pointer() { assert_eq!(true, Value(0xf8).is_pointer()); } @@ -169,21 +127,6 @@ mod tests { } #[test] - fn true_is_bool() { - assert_eq!(true, Value(0xa).is_bool()); - } - - #[test] - fn false_is_bool() { - assert_eq!(true, Value(2).is_bool()); - } - - #[test] - fn is_not_bool() { - assert_eq!(false, Value(0).is_bool()); - } - - #[test] fn true_from_bool() { assert_eq!(Value(0xa), Value::from_bool(true)); } @@ -192,34 +135,4 @@ mod tests { fn false_from_bool() { assert_eq!(Value(2), Value::from_bool(false)); } - - #[test] - fn true_to_bool() { - assert_eq!(Ok(true), Value(0xa).to_bool()); - } - - #[test] - fn false_to_bool() { - assert_eq!(Ok(false), Value(2).to_bool()); - } - - #[test] - fn is_char() { - assert_eq!(true, Value(0x6100000004).is_char()); - } - - #[test] - fn is_not_char() { - assert_eq!(false, Value(0).is_char()); - } - - #[test] - fn from_char() { - assert_eq!(Value(0x5800000004), Value::from_char('X')); - } - - #[test] - fn to_char() { - assert_eq!(Ok('😂'), Value(0x1f60200000004).to_char()); - } } diff --git a/bytecode/src/encoding.rs b/bytecode/src/encoding.rs index deacae5..8f49ab6 100644 --- a/bytecode/src/encoding.rs +++ b/bytecode/src/encoding.rs @@ -1,225 +1,170 @@ -use crate::bytecode::Op; +// This file must stay in sync with encoding.csc. +use crate::bytecode::{Arg, Local, Op}; +use crate::data::Value; +use std::io; use std::io::Read; -fn read_tag<T: Read>(prog: &mut T) -> Result<u64, String> { +fn read_const<T: Read>(prog: &mut T) -> Result<Value, String> { let mut buf = [0; 8]; match prog.read_exact(&mut buf) { Ok(()) => (), - Err(_) => { - return Err(String::from("error reading input")); + Err(e) => { + return Err(format!("error reading input: {}", e)); } }; - Ok(u64::from_le_bytes(buf)) + Ok(Value(u64::from_le_bytes(buf))) } -fn read_i64<T: Read>(prog: &mut T) -> Result<i64, String> { - let mut buf = [0; 8]; +fn read_local<T: Read>(prog: &mut T) -> Result<Local, String> { + let mut buf = [0; 1]; match prog.read_exact(&mut buf) { Ok(()) => (), - Err(_) => { - return Err(String::from("error reading input")); + Err(e) => { + return Err(format!("error reading input: {}", e)); } }; - Ok(i64::from_le_bytes(buf)) + Ok(Local(buf[0])) } -fn read_u8<T: Read>(prog: &mut T) -> Result<u8, String> { - let mut buf = vec![0]; +fn read_arg<T: Read>(prog: &mut T, is_const: bool) -> Result<Arg, String> { + if is_const { + let c = read_const(prog)?; + return Ok(Arg::Const(c)); + } + let l = read_local(prog)?; + Ok(Arg::L(l)) +} + +fn op_decoding<T: Read>(prog: &mut T) -> Result<Option<Op>, String> { + let mut buf = [0; 1]; match prog.read_exact(&mut buf) { Ok(()) => (), - Err(_) => { - return Err(String::from("error reading input")); + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { + return Ok(None); + } + Err(e) => { + return Err(format!("error reading input: {}", e)); } }; - Ok(buf[0]) -} - -fn op_decoding<T: Read>(prog: &mut T) -> Result<Op, String> { - let op = match read_tag(prog)? { - 1010 => Op::Const(read_i64(prog)?), - 1020 => Op::Add, - 1030 => Op::Sub, - 1040 => Op::Mul, - 1050 => Op::Div, - 1060 => Op::Mod, - 2010 => Op::Alloc, - 2020 => Op::Peek, - 2030 => Op::Poke, - 2040 => Op::PeekByte, - 2050 => Op::PokeByte, - 3010 => Op::Pop, - 3020 => Op::Local(read_u8(prog)?), - 4010 => Op::If(read_i64(prog)?), - 4020 => Op::Call(read_u8(prog)?), - 4030 => Op::Ret, - 4040 => Op::Exit, - 5010 => Op::PutC, - 5020 => Op::GetC, + let tag = buf[0] >> 2; + let arg1_const = buf[0] & 2 != 0; + let arg2_const = buf[0] & 1 != 0; + let op = match tag { + // Note: arguments are evaluated left to right. + 0 => Op::Mov(read_local(prog)?, read_arg(prog, arg1_const)?), + 1 => Op::JmpIf(read_arg(prog, arg1_const)?, read_arg(prog, arg2_const)?), + 2 => Op::Jmp(read_arg(prog, arg1_const)?), + 3 => Op::Alloc(read_local(prog)?, read_arg(prog, arg1_const)?), + 4 => Op::Peek( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 5 => Op::Poke( + read_arg(prog, arg1_const)?, + read_arg(prog, false)?, + read_arg(prog, arg2_const)?, + ), + 6 => Op::Add( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 7 => Op::Sub( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 8 => Op::Mul( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 9 => Op::Div( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 10 => Op::Mod( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 11 => Op::PeekByte( + read_local(prog)?, + read_arg(prog, arg1_const)?, + read_arg(prog, arg2_const)?, + ), + 12 => Op::PokeByte( + read_arg(prog, arg1_const)?, + read_arg(prog, false)?, + read_arg(prog, arg2_const)?, + ), + 13 => Op::Exit(read_arg(prog, arg1_const)?), + 14 => Op::AllocBytevector(read_local(prog)?, read_arg(prog, arg1_const)?), _ => { return Err(String::from("invalid opcode")); } }; - Ok(op) + Ok(Some(op)) } -pub fn decode(prog: &[u8]) -> Result<Vec<Op>, String> { - let mut reader = prog; +pub fn decode<T: Read>(prog: &mut T) -> Result<Vec<Op>, String> { let mut out = Vec::new(); - while reader.len() > 0 { - out.push(op_decoding(&mut reader)?); + loop { + match op_decoding(prog)? { + Some(op) => { + out.push(op); + } + None => { + return Ok(out); + } + } } - Ok(out) } mod tests { use super::*; + use Arg::*; + use Op::*; #[test] - fn decode_const() { + fn decode_mov_const() { assert_eq!( - Ok(vec![Op::Const(10)]), - decode(&vec![0xf2, 0x3, 0, 0, 0, 0, 0, 0, 0xa, 0, 0, 0, 0, 0, 0, 0]) + Ok(vec![Mov(Local(0), Const(Value::from_int(10)))]), + decode(&mut vec![0x2, 0, 0x15, 0, 0, 0, 0, 0, 0, 0].as_slice()) ); } #[test] - fn decode_add() { + fn decode_mov_local() { assert_eq!( - Ok(vec![Op::Add]), - decode(&vec![0xfc, 0x3, 0, 0, 0, 0, 0, 0]) + Ok(vec![Mov(Local(0), L(Local(1)))]), + decode(&mut vec![0, 0, 1].as_slice()), ); } #[test] - fn decode_sub() { - assert_eq!(Ok(vec![Op::Sub]), decode(&vec![0x6, 0x4, 0, 0, 0, 0, 0, 0])); - } - - #[test] - fn decode_mul() { + fn decode_jmpif() { assert_eq!( - Ok(vec![Op::Mul]), - decode(&vec![0x10, 0x4, 0, 0, 0, 0, 0, 0]) + Ok(vec![JmpIf(L(Local(0)), L(Local(1)))]), + decode(&mut vec![4, 0, 1].as_slice()) ); } #[test] - fn decode_div() { + fn decode_jmp() { assert_eq!( - Ok(vec![Op::Div]), - decode(&vec![0x1a, 0x4, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_mod() { - assert_eq!( - Ok(vec![Op::Mod]), - decode(&vec![0x24, 0x4, 0, 0, 0, 0, 0, 0]) + Ok(vec![Jmp(L(Local(0)))]), + decode(&mut vec![8, 0].as_slice()) ); } #[test] fn decode_alloc() { assert_eq!( - Ok(vec![Op::Alloc]), - decode(&vec![0xda, 0x7, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_peek() { - assert_eq!( - Ok(vec![Op::Peek]), - decode(&vec![0xe4, 0x7, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_poke() { - assert_eq!( - Ok(vec![Op::Poke]), - decode(&vec![0xee, 0x7, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_peek_byte() { - assert_eq!( - Ok(vec![Op::PeekByte]), - decode(&vec![0xf8, 0x7, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_poke_byte() { - assert_eq!( - Ok(vec![Op::PokeByte]), - decode(&vec![0x2, 0x8, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_pop() { - assert_eq!( - Ok(vec![Op::Pop]), - decode(&vec![0xc2, 0xb, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_local() { - assert_eq!( - Ok(vec![Op::Local(10)]), - decode(&vec![0xcc, 0xb, 0, 0, 0, 0, 0, 0, 0xa]) - ); - } - - #[test] - fn decode_if() { - assert_eq!( - Ok(vec![Op::If(10)]), - decode(&vec![0xaa, 0xf, 0, 0, 0, 0, 0, 0, 0xa, 0, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_call() { - assert_eq!( - Ok(vec![Op::Call(10)]), - decode(&vec![0xb4, 0xf, 0, 0, 0, 0, 0, 0, 0xa]) - ); - } - - #[test] - fn decode_ret() { - assert_eq!( - Ok(vec![Op::Ret]), - decode(&vec![0xbe, 0xf, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_exit() { - assert_eq!( - Ok(vec![Op::Exit]), - decode(&vec![0xc8, 0xf, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_putc() { - assert_eq!( - Ok(vec![Op::PutC]), - decode(&vec![0x92, 0x13, 0, 0, 0, 0, 0, 0]) - ); - } - - #[test] - fn decode_getc() { - assert_eq!( - Ok(vec![Op::GetC]), - decode(&vec![0x9c, 0x13, 0, 0, 0, 0, 0, 0]) + Ok(vec![Alloc(Local(0), L(Local(1)))]), + decode(&mut vec![12, 0, 1].as_slice()) ); } } diff --git a/bytecode/src/heap.rs b/bytecode/src/heap.rs index 1706d10..d32283e 100644 --- a/bytecode/src/heap.rs +++ b/bytecode/src/heap.rs @@ -32,6 +32,7 @@ pub struct Heap { heap: Vec<u64>, free_pointer: usize, spare_heap: Vec<u64>, + last_reachable_cells: usize, } impl Heap { @@ -40,13 +41,14 @@ impl Heap { heap: vec![0; 512], // 4 kB free_pointer: 0, spare_heap: vec![0; 512], + last_reachable_cells: 0, } } fn alloc_size(&mut self, p: Pointer) -> Result<usize, String> { let (u, _) = open_ptr(p); if u == 0 { - return Err("cannot get the size of a nil pointer"); + return Err(String::from("cannot get the size of a nil pointer")); } // Alloc size is stored just below the pointer. let size = self.heap[u - 1]; @@ -105,29 +107,12 @@ impl Heap { return Ok(()); } - fn collect_garbage( - &mut self, - size_hint: usize, - stack: &mut [Value], - locals: &mut [Value], - ) -> Result<(), String> { - const MAX_HEAP_SIZE: usize = 4 * 1024 * 1024; // 4 GB - // Always at least double the heap size (keeping in mind the max heap size). - let mut size_hint = size_hint; - if size_hint < self.heap.len() { - size_hint = self.heap.len(); - } - let mut new_heap_size = self.heap.len() + size_hint; - if new_heap_size > MAX_HEAP_SIZE / 2 { - new_heap_size = MAX_HEAP_SIZE / 2; - } - self.spare_heap.resize(new_heap_size, 0); + fn collect_garbage(&mut self, locals: &mut [Value]) -> Result<(), String> { + self.spare_heap.resize(self.heap.len(), 0); let mut spare_heap_ptr = 0; let mut rewrites = HashMap::new(); - self.walk_gc_roots(stack, &mut spare_heap_ptr, &mut rewrites)?; self.walk_gc_roots(locals, &mut spare_heap_ptr, &mut rewrites)?; - // Walk the stacks and rewrite. - rewrite_pointers(stack, &rewrites)?; + // Rewrite values. rewrite_pointers(locals, &rewrites)?; // Activate the new heap! std::mem::swap(&mut self.heap, &mut self.spare_heap); @@ -160,6 +145,7 @@ impl Heap { )?; } } + self.last_reachable_cells = spare_heap_ptr; // Done?? return Ok(()); } @@ -167,42 +153,34 @@ impl Heap { fn alloc_b( &mut self, n: usize, - stack: &mut [Value], locals: &mut [Value], bytevector_p: bool, ) -> Result<Pointer, String> { + if self.free_pointer > 2 * self.last_reachable_cells { + self.collect_garbage(locals)?; + } let n_cells = (n + 7) / 8; - if self.heap.len() - self.free_pointer < n_cells { - self.collect_garbage(n, stack, locals)?; + if self.free_pointer + n_cells + 1 > self.heap.len() { + self.heap.resize(self.free_pointer + n_cells + 1, 0); } - let len_p = &mut self.heap[self.free_pointer]?; + let len_p = &mut self.heap[self.free_pointer]; *len_p = u64::try_from(n).unwrap() << 1; if bytevector_p { *len_p |= 1; } self.free_pointer += 1; let p = Pointer(self.free_pointer * 8); - let n_cells = n / 8; - self.heap[self.free_pointer..self.free_pointer + n].fill(0); + self.heap[self.free_pointer..self.free_pointer + n_cells].fill(0); + self.free_pointer += n_cells; return Ok(p); } - pub fn alloc( - &mut self, - n: usize, - stack: &mut [Value], - locals: &mut [Value], - ) -> Result<Pointer, String> { - return self.alloc_b(n, stack, locals, false); + pub fn alloc(&mut self, n: usize, locals: &mut [Value]) -> Result<Pointer, String> { + return self.alloc_b(n * 8, locals, false); } - pub fn alloc_bytevector( - &mut self, - n: usize, - stack: &mut [Value], - locals: &mut [Value], - ) -> Result<Pointer, String> { - return self.alloc_b(n, stack, locals, true); + pub fn alloc_bytevector(&mut self, n: usize, locals: &mut [Value]) -> Result<Pointer, String> { + return self.alloc_b(n, locals, true); } pub fn peek(&self, p: Pointer) -> Result<Value, String> { @@ -218,16 +196,17 @@ impl Heap { let Value(x) = v; if u >= self.heap.len() { return Err(format!("invalid pointer {:x}", p)); + } self.heap[u] = x; return Ok(()); } - fn peek_byte(&self, p: Pointer) -> Result<u8, String> { + pub fn peek_byte(&self, p: Pointer) -> Result<u8, String> { let (word_cnt, word_offset) = open_ptr(p); if word_cnt >= self.heap.len() { return Err(format!("invalid pointer {:x}", p)); } - return (self.heap[word_cnt] >> word_offset * 8) as u8; + Ok((self.heap[word_cnt] >> word_offset * 8) as u8) } pub fn poke_byte(&mut self, u: u8, p: Pointer) -> Result<(), String> { @@ -237,6 +216,7 @@ impl Heap { } let surrounding_word = self.heap[word_cnt]; let mask = !(0xff << word_offset * 8); - self.heap[word_cnt] = surrounding_word & mask | u << word_offset * 8; + self.heap[word_cnt] = surrounding_word & mask | u64::from(u) << word_offset * 8; + Ok(()) } } diff --git a/bytecode/src/main.rs b/bytecode/src/main.rs index 683ff26..8315d5a 100644 --- a/bytecode/src/main.rs +++ b/bytecode/src/main.rs @@ -3,12 +3,8 @@ mod data; mod encoding; mod heap; -use std::io::Read; - fn main() { - let mut buf = Vec::new(); - std::io::stdin().lock().read_to_end(&mut buf).unwrap(); - let prog = encoding::decode(&buf).unwrap(); + let prog = encoding::decode(&mut std::io::stdin().lock()).unwrap(); let exit_code = bytecode::eval(&prog).unwrap(); std::process::exit(i32::from(exit_code)); } diff --git a/bytecode/src/stack.rs b/bytecode/src/stack.rs deleted file mode 100644 index 66d3f7c..0000000 --- a/bytecode/src/stack.rs +++ /dev/null @@ -1,33 +0,0 @@ -struct Stack { - v: Vec<u64>, -} - -impl Stack { - fn pop(&mut self) -> Result<u64, String> { - self.v.pop().ok_or(String::from("stack underflow")) - } - - fn pop_int(&mut self) -> Result<i64, String> { - Ok(self.pop()? as i64 >> 1) - } - - fn push_int(&mut self, i: i64) { - self.v.push((i as u64) << 1 | 1); - } - - fn pop_pointer(&mut self) -> Result<Pointer, String> { - Ok(Pointer::from_bytes(self.pop()?)) - } - - fn push_pointer(&mut self, p: Pointer) { - self.v.push(p.bytes()); - } - - fn pop_usize(&mut self) -> Result<usize, String> { - Ok(usize::try_from(self.pop()?).unwrap() >> 1) - } - - fn push_usize(&mut self, p: usize) { - self.v.push(u64::try_from(p).unwrap() << 1 | 1); - } -} |
