diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c90f5c9..6ba81e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: + - macos-latest # arm64 macOS, native backend + - ubuntu-24.04-arm # arm64 Linux, native backend + - ubuntu-latest # x86-64 Linux, native backend + - windows-latest # x86-64 Windows, native backend runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 @@ -46,6 +50,34 @@ jobs: run: nim r -d:fixedChecks tests/tests.nim - name: Test C++ determinism run: nim cpp -r -d:release tests/tests.nim + - name: Test the machine code encoders against the system assembler + run: | + nim r -d:release tests/test_arm64.nim + nim r -d:release tests/test_amd64.nim + - name: Test compiled programs against the interpreter + run: | + nim r -d:release tests/test_jit.nim + nim r -d:release tests/test_native.nim + nim r -d:danger tests/test_native.nim + nim r -d:fixedChecks tests/test_native.nim + - name: Run the whole suite as machine code + run: | + nim r -d:release -d:bassyNativeStrict tests/tests.nim + nim r -d:danger -d:bassyNativeStrict tests/tests.nim + nim r -d:fixedChecks -d:bassyNativeStrict tests/tests.nim + - name: Test that compiled programs cannot outrun the sandbox + run: | + nim r -d:release tests/test_jit_safety.nim + nim r -d:danger tests/test_jit_safety.nim + nim r -d:release -d:bassyNativeStrict tests/test_jit_safety.nim + - name: Test the same scripts with the interpreter forced + run: nim r -d:release -d:bassyNoJit tests/test_jit.nim + - name: Render the BASIC raytracer and check cross-platform determinism + run: nim r -d:release tests/bench_raytracer.nim + - name: Run the BASIC text workload and check cross-platform determinism + run: nim r -d:release tests/bench_strings.nim + - name: Report native compilation speedups + run: nim r -d:release tests/bench_jit.nim - name: Run examples run: | nim r examples/hello.nim diff --git a/.gitignore b/.gitignore index 446e076..6281cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ nimcache nimble.paths nimble.develop nimbledeps/ +tests/raytracer.ppm diff --git a/README.md b/README.md index 9f8be96..a4fa2d2 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,55 @@ Native callbacks are trusted Nim code and define the sandbox's capabilities. Val `run` executes until completion or an error. It does not yield after a budget is exhausted. On an error, state can contain partial changes. Use `restart` or `reset` before running again and choose how your application handles any host effects already performed. +## Native compilation + +On arm64 and x86-64, under macOS, Linux, and Windows, Bassy can compile a whole program to machine code. Call `compileNative` once after creating a runtime. It returns how many bytecode instructions were compiled, which is either all of them or zero. After that, `run` executes the machine code instead of the interpreter. + +```nim +import bassy + +let program = compile(""" +total = 0 +for i = 1 to 100000 + total = total + i mod 7 +next +""") +var runtime = initRuntime(program) +if runtime.compileNative() > 0: + echo "running as machine code" +discard runtime.run +``` + +Compiling is optional and changes nothing a script can observe. Results, both budgets, print output, string storage, and errors are the same as the interpreter's, including the exact failure and where it happened. Strings, printing, host calls, and any instruction that is about to fail run the interpreter's own code for that one instruction. `runtime.handedBack` counts how many instructions went that way. + +The compiler is written in Nim and needs no external compiler or library. Loops keep their globals in machine registers, and ordinary code keeps the values it is working on in registers from one instruction to the next. + +Measured speedups over the interpreter: + +| Workload | arm64 (Apple M4) | x86-64 (Windows CI) | +| --- | --- | --- | +| Tight integer loops | 25 to 35 times | 100 to 150 times | +| Raytracer, fixed point with subs and host calls | 6.5 times | 9.9 times | +| Text processing, mostly string functions | 2.5 times | 2.1 times | + +The interpreter runs slower on the CI machines, so the ratios there come out higher. + +The interpreter runs instead when there is no backend for the target, such as WebAssembly, when the program is built with `-d:bassyNoJit`, or when a runtime is compiled from inside a host callback while machine code is running on the same thread. + +Scripts cannot leave the sandbox through compiled code. Every global, register, and branch target the code uses is proved in range before any machine code is written, and a program that fails a check is not compiled. Array indexes are still checked while running, and every computed jump is checked against the program's length. The compiler also checks that Nim's memory layout is the one it writes against, and refuses to compile if not. + +Host callbacks remain trusted code. If one resizes the runtime's own storage while compiled code is running, the compiled program is retired and the rest of the run is interpreted. On Apple Silicon the code pages stay executable, and writes to them are only ever enabled on the thread doing the compiling, never while machine code is running on it. + +Compiled code is not counted in `Limits`. A program's machine code is capped at 64 MiB and is usually under 200 bytes per bytecode instruction. + +Two build switches help when testing. `-d:bassyNative` compiles every runtime automatically. `-d:bassyNativeStrict` also fails loudly if any program is not compiled, so a test suite proves it really ran as machine code: + +```sh +nim r -d:release -d:bassyNativeStrict tests/tests.nim +nim r -d:release tests/test_native.nim +nim r -d:release tests/test_jit_safety.nim +``` + ## Strings Create a `StringPool`, call `host.addStringFunctions(pool)`, compile with that host, then call `pool.bindProgram(program)` before running. See [strings.nim](examples/strings.nim) for a complete example. diff --git a/src/bassy.nim b/src/bassy.nim index 816c787..3ffca63 100644 --- a/src/bassy.nim +++ b/src/bassy.nim @@ -5,9 +5,9 @@ import std/[strutils, tables], - bassy/[numbers, texts, buffers] + bassy/[buffers, bytecode, jit, numbers, texts] -export numbers +export bytecode, jit, numbers const DefaultMaxStrings* = 256 @@ -144,77 +144,6 @@ type line: int32 column: int32 - Op = enum - MeterOp, - LoadImmediateOp, - LoadFixedOp, - LoadStringOp, - TextCallOp, - MoveOp, - LoadGlobalOp, - LoadHostDataOp, - StoreGlobalOp, - StoreGlobalImmediateOp, - MoveGlobalOp, - AddGlobalImmediateOp, - AddGlobalOp, - AddGlobalHostDataOp, - AddGlobalRegisterOp, - ModuloGlobalImmediateOp, - AddGlobalArrayGlobalIndexOp, - AddOp, - SubtractOp, - MultiplyOp, - DivideOp, - IntegerDivideOp, - ModuloOp, - NegateOp, - EqualOp, - NotEqualOp, - LessOp, - LessEqualOp, - GreaterOp, - GreaterEqualOp, - AndOp, - OrOp, - XorOp, - EqvOp, - ImpOp, - NotOp, - JumpOp, - JumpIfZeroOp, - JumpUnlessGlobalEqualImmediateOp, - JumpUnlessGlobalNotEqualImmediateOp, - JumpUnlessGlobalLessImmediateOp, - JumpUnlessGlobalLessEqualImmediateOp, - JumpUnlessGlobalGreaterImmediateOp, - JumpUnlessGlobalGreaterEqualImmediateOp, - JumpUnlessGlobalModuloEqualZeroOp, - ArrayGetOp, - ArraySetOp, - BufferGetOp, - BufferSetOp, - ArrayAddGlobalsOp, - SetArgumentOp, - SetArgumentImmediateOp, - SetArgumentGlobalOp, - HostCallOp, - CallOp, - GosubOp, - ReturnOp, - ReturnLabelOp, - ExitSubOp, - HaltOp, - PrintTextOp, - PrintValueOp, - PrintNewlineOp - - Instruction = object - op: Op - a: int32 - b: int32 - c: int32 - BasicArray = object name: string base: int32 @@ -296,6 +225,11 @@ type printedEvents: int64 allocatedBytes: int64 finished: bool + machine: Machine + machineShape: array[6, int] + printer: PrintProc + nativeError: ref Exception + handedBack: int64 ArrayView* = object runtime: Runtime @@ -3166,6 +3100,9 @@ proc clear(values: var seq[Value]) = if values.len > 0: zeroMem(addr values[0], values.len * sizeof(Value)) +proc compileNative*(runtime: var Runtime): int + ## Compiles this program to machine code; the body sits further down. + proc initRuntimeState( program: Program, host: Host, @@ -3305,6 +3242,21 @@ proc initRuntimeState( for i, function in program.hostFunctions: let id = host.functionIds.getOrDefault(function.name, -1'i32) result.hostCallbacks[i] = host.functions[int(id)].callback + when defined(bassyNative) or defined(bassyNativeStrict): + # Every runtime runs as machine code wherever the compiler accepts it, + # and anything it refuses stays on the interpreter, as usual. + if jitSupported(): + let compiled = result.compileNative() + when defined(bassyNativeStrict): + # For test suites only: a refusal fails the whole run, and not as a + # BasicError a test expecting one could mistake for its own, so a + # suite proves every program it holds really ran as machine code. + # The one refusal by design is a runtime made from inside compiled + # code, which must stay interpreted. + if compiled != program.code.len and not runningCompiledCode(): + raiseAssert("native compilation refused this program") + else: + discard compiled proc initRuntime*(program: Program, limits = defaultLimits()): Runtime = ## Allocates a runtime for a program without host bindings. @@ -3411,6 +3363,115 @@ proc instructions*(program: Program): int {.inline.} = ## Returns the number of metered register-machine instructions. program.code.len +proc routineExtents*(program: Program): seq[RoutineExtent] = + ## Returns where each routine's code sits and what a call to it needs. + for routine in program.routines: + result.add(RoutineExtent( + entry: routine.entry, + length: routine.codeLength, + registers: routine.registerCount, + parameters: routine.parameterCount + )) + +proc frameLayoutMatches*(): bool = + ## Confirms the frame layout compiled code would write by hand. These + ## offsets were read off this Nim version, and compiled code pushes and + ## pops frames the interpreter then reads, so a quiet change here would + ## corrupt the call stack rather than merely slow something down. + if sizeof(Frame) != FrameStride: + return false + var probe: Frame + let origin = cast[int](probe.addr) + if cast[int](probe.base.addr) - origin != FrameBase: + return false + if cast[int](probe.routine.addr) - origin != FrameRoutine: + return false + if cast[int](probe.returnPc.addr) - origin != FrameReturn: + return false + if cast[int](probe.kind.addr) - origin != FrameTag: + return false + ord(SubFrame) == 0 + +proc fixedConstants*(program: Program): seq[int32] = + ## Returns the raw bits of every fixed-point constant the code names. + for value in program.fixedValues: + result.add(int32(value)) + +proc handedBack*(runtime: Runtime): int64 {.inline.} = + ## Returns how many instructions compiled code has handed to the + ## interpreter's own code to run, because they were strings, printing, + ## host calls, or about to fail. Everything else ran as machine code. + runtime.handedBack + +proc storageShape(runtime: Runtime): array[6, int] {.raises: [].} = + ## Returns how long each store compiled code indexes directly is. The + ## compiler proved every index it emits against exactly these. + [runtime.globals.len, runtime.memory.len, runtime.hostData.len, + runtime.frames.len, runtime.arguments.len, runtime.registers.len] + +proc compileNative*(runtime: var Runtime): int = + ## Compiles this program to machine code and returns how many bytecode + ## offsets now run natively: all of them, or none where the target has + ## no backend or the compiler is not sure of the program. The + ## interpreter runs whatever is not compiled, and the two cannot be told + ## apart, so behavior never depends on the result. + runtime.machine = nil + if not jitSupported(): + return 0 + if not frameLayoutMatches(): + # Compiled code pushes and pops frames the interpreter then reads, so + # a layout it does not recognise means nothing may be compiled. + return 0 + var extents = newSeq[ArrayExtent](runtime.program.arrays.len) + for index, item in runtime.program.arrays: + extents[index] = ArrayExtent(base: item.base, length: item.length) + let limits = CallLimits( + frames: int32(runtime.frames.len), + slots: int32(runtime.registers.len) + ) + runtime.machine = compileProgram( + runtime.program.code, + runtime.program.routineExtents, + extents, + runtime.program.fixedConstants, + runtime.globals.len, + runtime.hostData.len, + runtime.arguments.len, + limits, + textLayoutMatches() + ) + if runtime.machine != nil: + runtime.machineShape = runtime.storageShape + return runtime.program.code.len + +proc arrayExtent*(program: Program, id: int32): (int32, int32) {.inline.} = + ## Returns where one array starts and how many cells it has. + (program.arrays[int(id)].base, program.arrays[int(id)].length) + +proc maxRegisterCount*(program: Program): int32 {.inline.} = + ## Returns the most register slots any routine in the program uses. + program.maxRegisters + +proc bytecode*(program: Program): lent seq[Instruction] {.inline.} = + ## Exposes the metered bytecode for tools and the native compiler. + program.code + +proc globalsAddress*(runtime: var Runtime): pointer {.inline.} = + ## Returns the base of the scalar global storage. + if runtime.globals.len == 0: nil else: runtime.globals[0].addr + +proc globalValue*(runtime: Runtime, index: int32): Value {.inline.} = + ## Reads one scalar global by index. + runtime.globals[int(index)] + +proc offset*(runtime: Runtime): int32 {.inline.} = + ## Returns the bytecode offset the runtime will execute next. + runtime.pc + +proc remainingBudget*(runtime: Runtime): (int64, int64) {.inline.} = + ## Returns the unspent instruction and work budgets. + (runtime.remainingInstructions, runtime.remainingWork) + proc globals*(program: Program): int {.inline.} = ## Returns the number of implicitly declared scalar globals. program.globalNames.len @@ -3908,373 +3969,522 @@ proc leaveFrame(runtime: var Runtime) = runtime.routine = frame.routine runtime.pc = frame.returnPc -proc run*(runtime: var Runtime, print: PrintProc = nil): RunStats = - ## Executes verified bytecode with bounded work, memory, calls, and output. - if runtime.finished: - return +template chargeMeter(runtime: Runtime, item: Instruction) = + ## Charges one block's budgets, refusing before charging either. let - startInstructions = runtime.remainingInstructions - startWork = runtime.remainingWork - startBytes = runtime.printedBytes - startEvents = runtime.printedEvents + cost = int64(item.a) + instructionCount = int64(item.b) + if runtime.remainingInstructions < instructionCount or + runtime.remainingWork < cost: + if runtime.remainingInstructions < instructionCount: + fail("BASIC instruction limit exceeded") + fail("BASIC work limit exceeded") + runtime.remainingInstructions -= instructionCount + runtime.remainingWork -= cost + +template callHost(runtime: Runtime, item: Instruction) = + ## Runs one host function and stores its answer. The interpreter, and + ## compiled code's own path for host calls, both expand this. template register(index: int32): untyped = runtime.registers[int(runtime.base + index)] - template fetch(): Instruction = - runtime.program.code[int(runtime.pc)] - while not runtime.finished: - var item = fetch() - if item.op == MeterOp: - let - cost = int64(item.a) - instructionCount = int64(item.b) - if runtime.remainingInstructions < instructionCount or - runtime.remainingWork < cost: - if runtime.remainingInstructions < instructionCount: - fail("BASIC instruction limit exceeded") - fail("BASIC work limit exceeded") - runtime.remainingInstructions -= instructionCount - runtime.remainingWork -= cost - inc runtime.pc - item = fetch() - case item.op - of MeterOp: - fail("BASIC bytecode contains consecutive meter instructions") - of LoadStringOp: - var handle = runtime.stringLiterals[int(item.b)] - if handle < 0: - let text = runtime.program.literals[int(item.b)] - runtime.chargeWork(int64(text.len)) - handle = runtime.strings.put(text).stringHandle - runtime.stringLiterals[int(item.b)] = handle - register(item.a) = stringValue(runtime.strings.empty.stringOwner, handle) - inc runtime.pc - of TextCallOp: - register(item.a) = runtime.textCall(TextFunction(item.b), int(item.c)) - inc runtime.pc - of LoadFixedOp: - register(item.a) = toValue(runtime.program.fixedValues[int(item.b)]) - inc runtime.pc - of LoadImmediateOp: - register(item.a) = item.b - inc runtime.pc - of MoveOp: - register(item.a) = register(item.b) - inc runtime.pc - of LoadGlobalOp: - register(item.a) = runtime.globals[int(item.b)] - inc runtime.pc - of LoadHostDataOp: - register(item.a) = runtime.hostData[int(item.b)] - inc runtime.pc - of StoreGlobalOp: - runtime.globals[int(item.a)] = register(item.b) - inc runtime.pc - of StoreGlobalImmediateOp: - runtime.globals[int(item.a)] = item.b - inc runtime.pc - of MoveGlobalOp: - runtime.globals[int(item.a)] = runtime.globals[int(item.b)] - inc runtime.pc - of AddGlobalImmediateOp: - runtime.globals[int(item.a)] = - runtime.addValue(runtime.globals[int(item.a)], item.b) - inc runtime.pc - of AddGlobalOp: - runtime.globals[int(item.a)] = - runtime.addValue( - runtime.globals[int(item.a)], - runtime.globals[int(item.b)] - ) - inc runtime.pc - of AddGlobalHostDataOp: - runtime.globals[int(item.a)] = - runtime.addValue( - runtime.globals[int(item.a)], - runtime.hostData[int(item.b)] - ) - inc runtime.pc - of AddGlobalRegisterOp: - runtime.globals[int(item.a)] = - runtime.addValue(runtime.globals[int(item.a)], register(item.b)) - inc runtime.pc - of ModuloGlobalImmediateOp: - runtime.globals[int(item.a)] = `mod`( - runtime.globals[int(item.b)], - item.c + let + functionId = int(item.b) + count = int( + runtime.program.hostFunctions[functionId].parameters + ) + # Read in place: copying the closures would count references and ask + # the cycle collector about them on every single call. + template callback(): untyped = runtime.hostCallbacks[functionId] + var value: Value + if callback.context != nil: + runtime.collectBuffers() + if count == 0: + value = callback.context(runtime, []) + else: + value = callback.context( + runtime, + runtime.arguments.toOpenArray(0, count - 1) ) - inc runtime.pc - of AddGlobalArrayGlobalIndexOp: - let index = runtime.checkedArrayIndex( - item.b, - runtime.globals[int(item.c)] + elif callback.numeric != nil: + if count == 0: + value = callback.numeric([]) + else: + value = callback.numeric(runtime.arguments.toOpenArray(0, count - 1)) + else: + for i in 0 ..< count: + runtime.integerArguments[i] = runtime.arguments[i].asInt + if count == 0: + value = callback.integer(EmptyArguments) + else: + value = callback.integer( + runtime.integerArguments.toOpenArray(0, count - 1) ) - runtime.globals[int(item.a)] = - runtime.addValue(runtime.globals[int(item.a)], runtime.memory[index]) - inc runtime.pc - of AddOp: - register(item.a) = runtime.addValue(register(item.b), register(item.c)) - inc runtime.pc - of SubtractOp: - register(item.a) = register(item.b) - register(item.c) - inc runtime.pc - of MultiplyOp: - register(item.a) = register(item.b) * register(item.c) - inc runtime.pc - of DivideOp: - register(item.a) = register(item.b) / register(item.c) - inc runtime.pc - of IntegerDivideOp: - register(item.a) = register(item.b) div register(item.c) - inc runtime.pc - of ModuloOp: - register(item.a) = `mod`(register(item.b), register(item.c)) - inc runtime.pc - of NegateOp: - register(item.a) = -register(item.b) - inc runtime.pc - of EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, GreaterEqualOp: - register(item.a) = runtime.compareValue( - item.op, - register(item.b), - register(item.c) + runtime.requireValue(value) + requireType(runtime.program.hostFunctions[functionId].name, value) + if item.a >= 0: + register(item.a) = value + inc runtime.pc + +template performOp(runtime: Runtime, item: Instruction, print: PrintProc) = + ## Executes one instruction after its block was charged. The interpreter + ## loop and compiled code's slow path both expand this, so the two run + ## the very same code and cannot come to disagree. + template register(index: int32): untyped = + runtime.registers[int(runtime.base + index)] + case item.op + of MeterOp: + fail("BASIC bytecode contains consecutive meter instructions") + of LoadStringOp: + var handle = runtime.stringLiterals[int(item.b)] + if handle < 0: + let text = runtime.program.literals[int(item.b)] + runtime.chargeWork(int64(text.len)) + handle = runtime.strings.put(text).stringHandle + runtime.stringLiterals[int(item.b)] = handle + register(item.a) = stringValue(runtime.strings.empty.stringOwner, handle) + inc runtime.pc + of TextCallOp: + register(item.a) = runtime.textCall(TextFunction(item.b), int(item.c)) + inc runtime.pc + of LoadFixedOp: + register(item.a) = toValue(runtime.program.fixedValues[int(item.b)]) + inc runtime.pc + of LoadImmediateOp: + register(item.a) = item.b + inc runtime.pc + of MoveOp: + register(item.a) = register(item.b) + inc runtime.pc + of LoadGlobalOp: + register(item.a) = runtime.globals[int(item.b)] + inc runtime.pc + of LoadHostDataOp: + register(item.a) = runtime.hostData[int(item.b)] + inc runtime.pc + of StoreGlobalOp: + runtime.globals[int(item.a)] = register(item.b) + inc runtime.pc + of StoreGlobalImmediateOp: + runtime.globals[int(item.a)] = item.b + inc runtime.pc + of MoveGlobalOp: + runtime.globals[int(item.a)] = runtime.globals[int(item.b)] + inc runtime.pc + of AddGlobalImmediateOp: + runtime.globals[int(item.a)] = + runtime.addValue(runtime.globals[int(item.a)], item.b) + inc runtime.pc + of AddGlobalOp: + runtime.globals[int(item.a)] = + runtime.addValue( + runtime.globals[int(item.a)], + runtime.globals[int(item.b)] ) + inc runtime.pc + of AddGlobalHostDataOp: + runtime.globals[int(item.a)] = + runtime.addValue( + runtime.globals[int(item.a)], + runtime.hostData[int(item.b)] + ) + inc runtime.pc + of AddGlobalRegisterOp: + runtime.globals[int(item.a)] = + runtime.addValue(runtime.globals[int(item.a)], register(item.b)) + inc runtime.pc + of ModuloGlobalImmediateOp: + runtime.globals[int(item.a)] = `mod`( + runtime.globals[int(item.b)], + item.c + ) + inc runtime.pc + of AddGlobalArrayGlobalIndexOp: + let index = runtime.checkedArrayIndex( + item.b, + runtime.globals[int(item.c)] + ) + runtime.globals[int(item.a)] = + runtime.addValue(runtime.globals[int(item.a)], runtime.memory[index]) + inc runtime.pc + of AddOp: + register(item.a) = runtime.addValue(register(item.b), register(item.c)) + inc runtime.pc + of SubtractOp: + register(item.a) = register(item.b) - register(item.c) + inc runtime.pc + of MultiplyOp: + register(item.a) = register(item.b) * register(item.c) + inc runtime.pc + of DivideOp: + register(item.a) = register(item.b) / register(item.c) + inc runtime.pc + of IntegerDivideOp: + register(item.a) = register(item.b) div register(item.c) + inc runtime.pc + of ModuloOp: + register(item.a) = `mod`(register(item.b), register(item.c)) + inc runtime.pc + of NegateOp: + register(item.a) = -register(item.b) + inc runtime.pc + of EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, GreaterEqualOp: + register(item.a) = runtime.compareValue( + item.op, + register(item.b), + register(item.c) + ) + inc runtime.pc + of AndOp: + register(item.a) = register(item.b) and register(item.c) + inc runtime.pc + of OrOp: + register(item.a) = register(item.b) or register(item.c) + inc runtime.pc + of XorOp: + register(item.a) = register(item.b) xor register(item.c) + inc runtime.pc + of EqvOp: + register(item.a) = eqv(register(item.b), register(item.c)) + inc runtime.pc + of ImpOp: + register(item.a) = imp(register(item.b), register(item.c)) + inc runtime.pc + of NotOp: + register(item.a) = not register(item.b) + inc runtime.pc + of JumpOp: + runtime.pc = item.a + of JumpIfZeroOp: + if register(item.a) == 0: + runtime.pc = item.b + else: inc runtime.pc - of AndOp: - register(item.a) = register(item.b) and register(item.c) - inc runtime.pc - of OrOp: - register(item.a) = register(item.b) or register(item.c) - inc runtime.pc - of XorOp: - register(item.a) = register(item.b) xor register(item.c) - inc runtime.pc - of EqvOp: - register(item.a) = eqv(register(item.b), register(item.c)) - inc runtime.pc - of ImpOp: - register(item.a) = imp(register(item.b), register(item.c)) - inc runtime.pc - of NotOp: - register(item.a) = not register(item.b) - inc runtime.pc - of JumpOp: - runtime.pc = item.a - of JumpIfZeroOp: - if register(item.a) == 0: - runtime.pc = item.b - else: - inc runtime.pc - of JumpUnlessGlobalEqualImmediateOp: - if runtime.globals[int(item.a)] != item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalNotEqualImmediateOp: - if runtime.globals[int(item.a)] == item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalLessImmediateOp: - if runtime.globals[int(item.a)] >= item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalLessEqualImmediateOp: - if runtime.globals[int(item.a)] > item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalGreaterImmediateOp: - if runtime.globals[int(item.a)] <= item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalGreaterEqualImmediateOp: - if runtime.globals[int(item.a)] < item.b: - runtime.pc = item.c - else: - inc runtime.pc - of JumpUnlessGlobalModuloEqualZeroOp: - if `mod`(runtime.globals[int(item.a)], item.b) != 0: - runtime.pc = item.c - else: - inc runtime.pc - of ArrayGetOp: - let index = runtime.checkedArrayIndex(item.b, register(item.c)) - register(item.a) = runtime.memory[index] - inc runtime.pc - of ArraySetOp: - let index = runtime.checkedArrayIndex(item.a, register(item.b)) - runtime.memory[index] = register(item.c) + of JumpUnlessGlobalEqualImmediateOp: + if runtime.globals[int(item.a)] != item.b: + runtime.pc = item.c + else: inc runtime.pc - of ArrayAddGlobalsOp: - let index = runtime.checkedArrayIndex( - item.a, - runtime.globals[int(item.b)] - ) - runtime.memory[index] = - runtime.addValue(runtime.memory[index], runtime.globals[int(item.c)]) + of JumpUnlessGlobalNotEqualImmediateOp: + if runtime.globals[int(item.a)] == item.b: + runtime.pc = item.c + else: inc runtime.pc - of SetArgumentOp: - runtime.arguments[int(item.a)] = register(item.b) + of JumpUnlessGlobalLessImmediateOp: + if runtime.globals[int(item.a)] >= item.b: + runtime.pc = item.c + else: inc runtime.pc - of SetArgumentImmediateOp: - runtime.arguments[int(item.a)] = item.b + of JumpUnlessGlobalLessEqualImmediateOp: + if runtime.globals[int(item.a)] > item.b: + runtime.pc = item.c + else: inc runtime.pc - of SetArgumentGlobalOp: - runtime.arguments[int(item.a)] = runtime.globals[int(item.b)] + of JumpUnlessGlobalGreaterImmediateOp: + if runtime.globals[int(item.a)] <= item.b: + runtime.pc = item.c + else: inc runtime.pc - of BufferGetOp: - if register(item.b).kind != ArrayValue: - fail("BASIC indexed value must be a returned array") - let view = runtime.arrayView(register(item.b)) - register(item.a) = view[int(register(item.c).asInt)] + of JumpUnlessGlobalGreaterEqualImmediateOp: + if runtime.globals[int(item.a)] < item.b: + runtime.pc = item.c + else: inc runtime.pc - of BufferSetOp: - if register(item.a).kind != ArrayValue: - fail("BASIC indexed value must be a returned array") - let view = runtime.arrayView(register(item.a), writable = true) - view[int(register(item.b).asInt)] = register(item.c) + of JumpUnlessGlobalModuloEqualZeroOp: + if `mod`(runtime.globals[int(item.a)], item.b) != 0: + runtime.pc = item.c + else: inc runtime.pc - of HostCallOp: - let - functionId = int(item.b) - count = int( - runtime.program.hostFunctions[functionId].parameters - ) - callback = runtime.hostCallbacks[functionId] - var value: Value - if callback.context != nil: - runtime.collectBuffers() - if count == 0: - value = callback.context(runtime, []) - else: - value = callback.context( - runtime, - runtime.arguments.toOpenArray(0, count - 1) - ) - elif callback.numeric != nil: - if count == 0: - value = callback.numeric([]) + of ArrayGetOp: + let index = runtime.checkedArrayIndex(item.b, register(item.c)) + register(item.a) = runtime.memory[index] + inc runtime.pc + of ArraySetOp: + let index = runtime.checkedArrayIndex(item.a, register(item.b)) + runtime.memory[index] = register(item.c) + inc runtime.pc + of BufferGetOp: + if register(item.b).kind != ArrayValue: + fail("BASIC indexed value must be a returned array") + let view = runtime.arrayView(register(item.b)) + register(item.a) = view[int(register(item.c).asInt)] + inc runtime.pc + of BufferSetOp: + if register(item.a).kind != ArrayValue: + fail("BASIC indexed value must be a returned array") + let view = runtime.arrayView(register(item.a), writable = true) + view[int(register(item.b).asInt)] = register(item.c) + inc runtime.pc + of ArrayAddGlobalsOp: + let index = runtime.checkedArrayIndex( + item.a, + runtime.globals[int(item.b)] + ) + runtime.memory[index] = + runtime.addValue(runtime.memory[index], runtime.globals[int(item.c)]) + inc runtime.pc + of SetArgumentOp: + runtime.arguments[int(item.a)] = register(item.b) + inc runtime.pc + of SetArgumentImmediateOp: + runtime.arguments[int(item.a)] = item.b + inc runtime.pc + of SetArgumentGlobalOp: + runtime.arguments[int(item.a)] = runtime.globals[int(item.b)] + inc runtime.pc + of HostCallOp: + runtime.callHost(item) + of CallOp, GosubOp: + if runtime.depth + 1 >= int32(runtime.frames.len): + fail("BASIC call depth limit exceeded") + let + callee = + if item.op == GosubOp: + runtime.routine else: - value = callback.numeric(runtime.arguments.toOpenArray(0, count - 1)) - else: - for i in 0 ..< count: - runtime.integerArguments[i] = runtime.arguments[i].asInt - if count == 0: - value = callback.integer(EmptyArguments) + item.a + calleeRegisters = + runtime.program.routines[int(callee)].registerCount + calleeParameters = + int(runtime.program.routines[int(callee)].parameterCount) + calleeEntry = + if item.op == GosubOp: + item.a else: - value = callback.integer( - runtime.integerArguments.toOpenArray(0, count - 1) - ) - runtime.requireValue(value) - requireType(runtime.program.hostFunctions[functionId].name, value) - if item.a >= 0: - register(item.a) = value - inc runtime.pc - of CallOp, GosubOp: - if runtime.depth + 1 >= int32(runtime.frames.len): - fail("BASIC call depth limit exceeded") - let - callee = - if item.op == GosubOp: - runtime.routine - else: - item.a - calleeRegisters = - runtime.program.routines[int(callee)].registerCount - calleeParameters = - int(runtime.program.routines[int(callee)].parameterCount) - calleeEntry = - if item.op == GosubOp: - item.a - else: - runtime.program.routines[int(callee)].entry - callerRegisters = - runtime.program.routines[int(runtime.routine)].registerCount - nextBase = runtime.base + callerRegisters - nextEnd = nextBase + calleeRegisters - if nextEnd > int32(runtime.registers.len): - fail("BASIC register stack exceeds its memory limit") - runtime.frames[int(runtime.depth)] = Frame( - base: runtime.base, - routine: runtime.routine, - returnPc: runtime.pc + 1, - kind: - if item.op == GosubOp: - GosubFrame - else: - SubFrame - ) - if calleeRegisters > 0: + runtime.program.routines[int(callee)].entry + callerRegisters = + runtime.program.routines[int(runtime.routine)].registerCount + nextBase = runtime.base + callerRegisters + nextEnd = nextBase + calleeRegisters + if nextEnd > int32(runtime.registers.len): + fail("BASIC register stack exceeds its memory limit") + runtime.frames[int(runtime.depth)] = Frame( + base: runtime.base, + routine: runtime.routine, + returnPc: runtime.pc + 1, + kind: if item.op == GosubOp: - copyMem( - addr runtime.registers[int(nextBase)], - addr runtime.registers[int(runtime.base)], - int(calleeRegisters) * sizeof(Value) - ) + GosubFrame else: - zeroMem( - addr runtime.registers[int(nextBase)], - int(calleeRegisters) * sizeof(Value) - ) - if item.op == CallOp: - for i in 0 ..< calleeParameters: - runtime.registers[int(nextBase) + i] = runtime.arguments[i] - inc runtime.depth - runtime.base = nextBase - runtime.routine = callee - runtime.pc = calleeEntry - of ReturnOp: - runtime.leaveFrame - of ReturnLabelOp: - if runtime.depth == 0 or - runtime.frames[int(runtime.depth) - 1].kind != GosubFrame: - fail("BASIC RETURN label without GOSUB") - runtime.leaveFrame - runtime.pc = item.a - of ExitSubOp: - while runtime.depth > 0 and - runtime.frames[int(runtime.depth) - 1].kind == GosubFrame: - runtime.leaveFrame - runtime.leaveFrame - of HaltOp: - runtime.finished = true - of PrintTextOp: - runtime.chargePrint( - int64(runtime.program.literals[int(item.a)].len) - ) + SubFrame + ) + if calleeRegisters > 0: + if item.op == GosubOp: + copyMem( + addr runtime.registers[int(nextBase)], + addr runtime.registers[int(runtime.base)], + int(calleeRegisters) * sizeof(Value) + ) + else: + zeroMem( + addr runtime.registers[int(nextBase)], + int(calleeRegisters) * sizeof(Value) + ) + if item.op == CallOp: + for i in 0 ..< calleeParameters: + runtime.registers[int(nextBase) + i] = runtime.arguments[i] + inc runtime.depth + runtime.base = nextBase + runtime.routine = callee + runtime.pc = calleeEntry + of ReturnOp: + runtime.leaveFrame + of ReturnLabelOp: + if runtime.depth == 0 or + runtime.frames[int(runtime.depth) - 1].kind != GosubFrame: + fail("BASIC RETURN label without GOSUB") + runtime.leaveFrame + runtime.pc = item.a + of ExitSubOp: + while runtime.depth > 0 and + runtime.frames[int(runtime.depth) - 1].kind == GosubFrame: + runtime.leaveFrame + runtime.leaveFrame + of HaltOp: + runtime.finished = true + of PrintTextOp: + runtime.chargePrint( + int64(runtime.program.literals[int(item.a)].len) + ) + if print != nil: + print(PrintEvent( + kind: TextPrint, + text: runtime.program.literals[int(item.a)] + )) + inc runtime.pc + of PrintValueOp: + let value = register(item.a) + if value.kind == StringValue: + let length = int64(runtime.strings.length(value)) + runtime.chargeWork(length) + runtime.chargePrint(length) + if print != nil: + print(PrintEvent(kind: TextPrint, text: runtime.strings.get(value))) + elif value.kind == FixedValue: + let text = $value + runtime.chargePrint(int64(text.len)) if print != nil: print(PrintEvent( - kind: TextPrint, - text: runtime.program.literals[int(item.a)] + kind: FixedPrint, fixedValue: value.asFixed, text: text )) - inc runtime.pc - of PrintValueOp: - let value = register(item.a) - if value.kind == StringValue: - let length = int64(runtime.strings.length(value)) - runtime.chargeWork(length) - runtime.chargePrint(length) - if print != nil: - print(PrintEvent(kind: TextPrint, text: runtime.strings.get(value))) - elif value.kind == FixedValue: - let text = $value - runtime.chargePrint(int64(text.len)) - if print != nil: - print(PrintEvent( - kind: FixedPrint, fixedValue: value.asFixed, text: text - )) - else: - runtime.chargePrint(printedIntegerBytes(value.asInt)) - if print != nil: - print(PrintEvent(kind: ValuePrint, value: value.asInt)) - inc runtime.pc - of PrintNewlineOp: - runtime.chargePrint(1) + else: + runtime.chargePrint(printedIntegerBytes(value.asInt)) if print != nil: - print(PrintEvent(kind: NewlinePrint)) + print(PrintEvent(kind: ValuePrint, value: value.asInt)) + inc runtime.pc + of PrintNewlineOp: + runtime.chargePrint(1) + if print != nil: + print(PrintEvent(kind: NewlinePrint)) + inc runtime.pc + +proc publishStorage(runtime: Runtime, context: ptr NativeContext) + {.raises: [].} = + ## Tells compiled code where the runtime's storage is now. Called before + ## compiled code starts and after every call out of it, since host code + ## is free to replace a buffer in between. + template first(values: untyped): pointer = + if values.len == 0: nil else: values[0].addr + context.globals = first(runtime.globals) + context.memory = first(runtime.memory) + context.hostData = first(runtime.hostData) + context.frames = first(runtime.frames) + context.arguments = first(runtime.arguments) + context.registerFile = first(runtime.registers) + +template handOver(context: ptr NativeContext, pc: int32, + body: untyped): int32 = + ## Runs interpreter code for compiled code. Compiled code keeps the frame + ## and budgets in the context, so they are carried in and back out + ## around it. A failure cannot travel back through frames compiled code + ## built, so it is kept and raised again once compiled code returns. + # A cursor, so no reference is counted and no cycle root registered on + # every call; the runtime is alive for as long as compiled code runs. + var runtime {.cursor, inject.} = cast[Runtime](context.runtime) + inc runtime.handedBack + runtime.pc = pc + runtime.base = context.base + runtime.depth = context.depth + runtime.routine = context.routine + runtime.remainingInstructions = context.remainingInstructions + runtime.remainingWork = context.remainingWork + var status = 0'i32 + try: + # An offset that is not the program's own cannot be looked up. The + # compiler proves none is ever reached, and this makes a missed proof + # an ordinary refusal rather than a defect the host did not expect. + if pc < 0 or int(pc) >= runtime.program.code.len: + fail("BASIC compiled code reached an offset outside the program") + body + except Exception as error: + runtime.nativeError = error + status = 1 + if status == 0 and runtime.storageShape != runtime.machineShape: + # Host code resized storage the compiled program indexes by offsets + # proved at compile time. None of them can be trusted now, so the + # program is retired and the rest of the run is interpreted, where + # every access is checked. + runtime.machine = nil + status = int32(ord(NativeRetired)) + if status == 0: + context.pc = runtime.pc + runtime.publishStorage(context) + context.base = runtime.base + context.depth = runtime.depth + context.routine = runtime.routine + context.remainingInstructions = runtime.remainingInstructions + context.remainingWork = runtime.remainingWork + status + +proc nativeStep(context: ptr NativeContext, pc: int32): int32 {.cdecl.} = + ## Runs any one instruction for compiled code, through the very code the + ## interpreter runs for it. + handOver(context, pc): + let item = runtime.program.code[int(pc)] + if item.op == MeterOp: + runtime.chargeMeter(item) + inc runtime.pc + else: + runtime.performOp(item, runtime.printer) + +proc nativeHostCall(context: ptr NativeContext, pc: int32): int32 + {.cdecl.} = + ## Runs one host call for compiled code, going straight to the host + ## call's own code rather than through the general dispatch. + handOver(context, pc): + runtime.callHost(runtime.program.code[int(pc)]) + +proc runMachine(runtime: var Runtime, print: PrintProc) = + ## Runs the compiled program from wherever the runtime stands until it + ## halts, or raises whatever the interpreter's code raised on its way. + if runtime.storageShape != runtime.machineShape: + # Storage changed shape since compiling, so the program is retired + # before it runs rather than part way through. + runtime.machine = nil + return + # Held here as well, so retiring the program part way cannot free the + # code it is still returning through. + let machine = runtime.machine + var context = NativeContext( + table: runtime.machine.tableAddress, + base: runtime.base, + depth: runtime.depth, + routine: runtime.routine, + runtime: cast[pointer](runtime), + step: cast[pointer](nativeStep), + hostStep: cast[pointer](nativeHostCall), + stringOwner: runtime.strings.ownerAddress, + stringSpans: runtime.strings.spansAddress, + stringArena: runtime.strings.arenaAddress, + remainingInstructions: runtime.remainingInstructions, + remainingWork: runtime.remainingWork, + pc: runtime.pc + ) + runtime.publishStorage(context.addr) + runtime.printer = print + enterCompiledCode() + let status = + try: + machine.invoke(context) + finally: + leaveCompiledCode() + runtime.printer = nil + if status == NativeFailed: + # The interpreter's code left the runtime exactly as it failed. + let error = runtime.nativeError + runtime.nativeError = nil + raise error + if status == NativeRetired: + # The interpreter's code left the runtime where it stopped, and the + # run carries on interpreted from there. + return + runtime.remainingInstructions = context.remainingInstructions + runtime.remainingWork = context.remainingWork + runtime.pc = context.pc + runtime.base = context.base + runtime.depth = context.depth + runtime.routine = context.routine + runtime.finished = true + +proc run*(runtime: var Runtime, print: PrintProc = nil): RunStats = + ## Executes verified bytecode with bounded work, memory, calls, and output. + if runtime.finished: + return + let + startInstructions = runtime.remainingInstructions + startWork = runtime.remainingWork + startBytes = runtime.printedBytes + startEvents = runtime.printedEvents + template fetch(): Instruction = + runtime.program.code[int(runtime.pc)] + if runtime.machine != nil: + runtime.runMachine(print) + while not runtime.finished: + var item = fetch() + if item.op == MeterOp: + runtime.chargeMeter(item) inc runtime.pc + item = fetch() + runtime.performOp(item, print) result = RunStats( instructions: startInstructions - runtime.remainingInstructions, workUnits: startWork - runtime.remainingWork, diff --git a/src/bassy/amd64.nim b/src/bassy/amd64.nim new file mode 100644 index 0000000..851f406 --- /dev/null +++ b/src/bassy/amd64.nim @@ -0,0 +1,472 @@ +## Encodes the x86-64 subset the BASIC compiler needs. +## Instructions vary in length, so labels record the displacement field +## and patch it once every target is placed. + +import numbers + +type + Register* = distinct uint32 + + Width* = enum + ## Selects the 32-bit or 64-bit operand size. + Word32, + Word64 + + Condition* = enum + ## Branch conditions, in architectural encoding order. + OverflowCondition, + NoOverflowCondition, + BelowCondition, + AboveEqualCondition, + EqualCondition, + NotEqualCondition, + BelowEqualCondition, + AboveCondition, + SignCondition, + NoSignCondition, + ParityCondition, + NoParityCondition, + LessCondition, + GreaterEqualCondition, + LessEqualCondition, + GreaterCondition + + Label* = distinct int + + Fixup = object + at: int + next: int + label: int + + Assembler* = object + ## Collects encoded bytes plus unresolved label references. + code*: seq[byte] + targets: seq[int] + fixups: seq[Fixup] + +const + rax* = Register(0) + rcx* = Register(1) + rdx* = Register(2) + rbx* = Register(3) + rsp* = Register(4) + rbp* = Register(5) + rsi* = Register(6) + rdi* = Register(7) + r8* = Register(8) + r9* = Register(9) + r10* = Register(10) + r11* = Register(11) + r12* = Register(12) + r13* = Register(13) + r14* = Register(14) + r15* = Register(15) + +proc number(register: Register): uint32 {.inline, raises: [].} = + ## Returns the four-bit encoding of a register. + uint32(register) and 15'u32 + +proc fail(message: string) {.noreturn, raises: [BasicError].} = + ## Reports a controlled encoding failure. + raise newException(BasicError, "BASIC " & message) + +proc emit(assembler: var Assembler, value: byte) {.inline, raises: [].} = + ## Appends one encoded byte. + assembler.code.add(value) + +proc emitDouble(assembler: var Assembler, value: int32) {.raises: [].} = + ## Appends a little-endian 32-bit field. + let bits = cast[uint32](value) + for shift in [0, 8, 16, 24]: + assembler.emit(byte((bits shr shift) and 0xFF'u32)) + +proc emitQuad(assembler: var Assembler, value: int64) {.raises: [].} = + ## Appends a little-endian 64-bit field. + let bits = cast[uint64](value) + for shift in [0, 8, 16, 24, 32, 40, 48, 56]: + assembler.emit(byte((bits shr shift) and 0xFF'u64)) + +proc position*(assembler: Assembler): int {.inline, raises: [].} = + ## Returns the offset of the next byte. + assembler.code.len + +## Prefixes and operand encoding + +proc prefix(assembler: var Assembler, width: Width, reg, rm: Register) + {.raises: [].} = + ## Emits a REX prefix when the operands or the width require one. + var value = 0x40'u32 + if width == Word64: + value = value or 0x08'u32 + if reg.number >= 8: + value = value or 0x04'u32 + if rm.number >= 8: + value = value or 0x01'u32 + if value != 0x40'u32: + assembler.emit(byte(value)) + +proc directOperand(assembler: var Assembler, reg, rm: Register) + {.raises: [].} = + ## Encodes a register-to-register operand pair. + assembler.emit( + byte(0xC0'u32 or ((reg.number and 7'u32) shl 3) or (rm.number and 7'u32)) + ) + +proc memoryOperand(assembler: var Assembler, reg, base: Register, + displacement: int) {.raises: [BasicError].} = + ## Encodes a register plus a base register with a displacement. + if base.number == 4 or base.number == 12: + fail("assembler cannot address through this base register") + let low = (reg.number and 7'u32) shl 3 + let rm = base.number and 7'u32 + # An r13 base always needs an explicit displacement byte. + if displacement == 0 and rm != 5: + assembler.emit(byte(0x00'u32 or low or rm)) + elif displacement >= -128 and displacement <= 127: + assembler.emit(byte(0x40'u32 or low or rm)) + assembler.emit(byte(cast[uint8](int8(displacement)))) + else: + assembler.emit(byte(0x80'u32 or low or rm)) + assembler.emitDouble(int32(displacement)) + +## Labels + +proc label*(assembler: var Assembler): Label {.raises: [].} = + ## Reserves an unplaced branch target. + assembler.targets.add(-1) + Label(assembler.targets.len - 1) + +proc place*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Fixes a label at the current byte offset. + assembler.targets[int(target)] = assembler.code.len + +proc resolve*(assembler: var Assembler) {.raises: [BasicError].} = + ## Patches every recorded displacement once all labels are placed. + for fixup in assembler.fixups: + let destination = assembler.targets[fixup.label] + if destination < 0: + fail("assembler label was never placed") + let distance = destination - fixup.next + if distance < low(int32) or distance > high(int32): + fail("assembler branch is out of range") + let bits = cast[uint32](int32(distance)) + for index in 0 ..< 4: + assembler.code[fixup.at + index] = + byte((bits shr (index * 8)) and 0xFF'u32) + assembler.fixups.setLen(0) + +## Moves + +proc moveRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Copies one register into another. + assembler.prefix(width, source, destination) + assembler.emit(0x89) + assembler.directOperand(source, destination) + +proc loadImmediate*(assembler: var Assembler, width: Width, + destination: Register, value: int64) {.raises: [].} = + ## Materializes a constant, using the shortest form that holds it. + if width == Word32 or (value >= 0 and value <= high(int32)): + if destination.number >= 8: + assembler.emit(0x41) + assembler.emit(byte(0xB8'u32 + (destination.number and 7'u32))) + assembler.emitDouble(int32(value)) + else: + assembler.prefix(Word64, Register(0), destination) + assembler.emit(byte(0xB8'u32 + (destination.number and 7'u32))) + assembler.emitQuad(value) + +## Memory + +proc loadWord*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads 32 bits into the destination. + assembler.prefix(Word32, destination, base) + assembler.emit(0x8B) + assembler.memoryOperand(destination, base, offset) + +proc storeWord*(assembler: var Assembler, source, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Stores the low 32 bits of a register. + assembler.prefix(Word32, source, base) + assembler.emit(0x89) + assembler.memoryOperand(source, base, offset) + +proc loadDouble*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads 64 bits into the destination. + assembler.prefix(Word64, destination, base) + assembler.emit(0x8B) + assembler.memoryOperand(destination, base, offset) + +proc storeDouble*(assembler: var Assembler, source, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Stores 64 bits from a register. + assembler.prefix(Word64, source, base) + assembler.emit(0x89) + assembler.memoryOperand(source, base, offset) + +proc loadByteZeroed*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads one byte, zero-extending it into the destination. + assembler.prefix(Word32, destination, base) + assembler.emit(0x0F) + assembler.emit(0xB6) + assembler.memoryOperand(destination, base, offset) + +proc storeByteImmediate*(assembler: var Assembler, base: Register, + offset: int, value: byte) {.raises: [BasicError].} = + ## Stores a constant byte through a base register. + assembler.prefix(Word32, Register(0), base) + assembler.emit(0xC6) + assembler.memoryOperand(Register(0), base, offset) + assembler.emit(value) + +## Arithmetic + +proc addRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Adds the source into the destination. + assembler.prefix(width, source, destination) + assembler.emit(0x01) + assembler.directOperand(source, destination) + +proc subtractRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Subtracts the source from the destination. + assembler.prefix(width, source, destination) + assembler.emit(0x29) + assembler.directOperand(source, destination) + +proc groupImmediate(assembler: var Assembler, width: Width, + extension: uint32, target: Register, value: int32) {.raises: [].} = + ## Encodes one of the immediate arithmetic forms by its opcode extension. + assembler.prefix(width, Register(extension), target) + assembler.emit(0x81) + assembler.directOperand(Register(extension), target) + assembler.emitDouble(value) + +proc addImmediate*(assembler: var Assembler, width: Width, + target: Register, value: int32) {.raises: [].} = + ## Adds a constant to a register. + assembler.groupImmediate(width, 0, target, value) + +proc subtractImmediate*(assembler: var Assembler, width: Width, + target: Register, value: int32) {.raises: [].} = + ## Subtracts a constant from a register. + assembler.groupImmediate(width, 5, target, value) + +proc compareImmediate*(assembler: var Assembler, width: Width, + target: Register, value: int32) {.raises: [].} = + ## Sets flags from a register against a constant. + assembler.groupImmediate(width, 7, target, value) + +proc compareRegister*(assembler: var Assembler, width: Width, + left, right: Register) {.raises: [].} = + ## Sets flags from the difference of two registers. + assembler.prefix(width, right, left) + assembler.emit(0x39) + assembler.directOperand(right, left) + +proc testRegister*(assembler: var Assembler, width: Width, + left, right: Register) {.raises: [].} = + ## Sets flags from the conjunction of two registers. + assembler.prefix(width, right, left) + assembler.emit(0x85) + assembler.directOperand(right, left) + +proc multiplyRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Multiplies the destination by the source. + assembler.prefix(width, destination, source) + assembler.emit(0x0F) + assembler.emit(0xAF) + assembler.directOperand(destination, source) + +proc signExtendToPair*(assembler: var Assembler, width: Width) + {.raises: [].} = + ## Widens the accumulator into the high half before a signed divide. + if width == Word64: + assembler.emit(0x48) + assembler.emit(0x99) + +proc signedDivide*(assembler: var Assembler, width: Width, + divisor: Register) {.raises: [].} = + ## Divides the widened accumulator, leaving the remainder in rdx. + assembler.prefix(width, Register(7), divisor) + assembler.emit(0xF7) + assembler.directOperand(Register(7), divisor) + +## Stack and control flow + +proc push*(assembler: var Assembler, source: Register) {.raises: [].} = + ## Pushes a 64-bit register. + if source.number >= 8: + assembler.emit(0x41) + assembler.emit(byte(0x50'u32 + (source.number and 7'u32))) + +proc pop*(assembler: var Assembler, destination: Register) {.raises: [].} = + ## Pops a 64-bit register. + if destination.number >= 8: + assembler.emit(0x41) + assembler.emit(byte(0x58'u32 + (destination.number and 7'u32))) + +proc branch*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Jumps unconditionally to a label. + assembler.emit(0xE9) + assembler.fixups.add( + Fixup(at: assembler.code.len, next: assembler.code.len + 4, + label: int(target)) + ) + assembler.emitDouble(0) + +proc branchIf*(assembler: var Assembler, condition: Condition, + target: Label) {.raises: [].} = + ## Jumps to a label when the condition holds. + assembler.emit(0x0F) + assembler.emit(byte(0x80'u32 + uint32(ord(condition)))) + assembler.fixups.add( + Fixup(at: assembler.code.len, next: assembler.code.len + 4, + label: int(target)) + ) + assembler.emitDouble(0) + +proc returnToCaller*(assembler: var Assembler) {.raises: [].} = + ## Returns to the caller. + assembler.emit(0xC3) + +proc storeWordImmediate*(assembler: var Assembler, base: Register, + offset: int, value: int32) {.raises: [BasicError].} = + ## Stores a 32-bit constant through a base register. + assembler.prefix(Word32, Register(0), base) + assembler.emit(0xC7) + assembler.memoryOperand(Register(0), base, offset) + assembler.emitDouble(value) + +proc negateRegister*(assembler: var Assembler, width: Width, + target: Register) {.raises: [].} = + ## Replaces a register with its two's complement negation. + assembler.prefix(width, Register(3), target) + assembler.emit(0xF7) + assembler.directOperand(Register(3), target) + +proc setIfCondition*(assembler: var Assembler, target: Register, + condition: Condition) {.raises: [].} = + ## Writes one when the condition holds and zero otherwise. + ## The low byte is set, so the register is cleared first; xor would + ## disturb the flags, and movzx afterwards would need a second register. + ## A REX prefix is forced for the byte form, so rsp, rbp, rsi and rdi + ## name their low bytes rather than ah, ch, dh and bh. + let rex = byte(0x40'u32 or (target.number shr 3)) + assembler.emit(rex) + assembler.emit(0x0F) + assembler.emit(byte(0x90'u32 + uint32(ord(condition)))) + assembler.directOperand(Register(0), target) + assembler.emit(byte(0x40'u32 or ((target.number shr 3) shl 2) or + (target.number shr 3))) + assembler.emit(0x0F) + assembler.emit(0xB6) + assembler.directOperand(target, target) + +proc shiftLeftImmediate*(assembler: var Assembler, width: Width, + target: Register, count: int) {.raises: [BasicError].} = + ## Shifts a register left by a constant. + if count < 0 or count > 63: + fail("assembler shift count is out of range") + assembler.prefix(width, Register(4), target) + assembler.emit(0xC1) + assembler.directOperand(Register(4), target) + assembler.emit(byte(count)) + +proc signExtendDouble*(assembler: var Assembler, + destination, source: Register) {.raises: [].} = + ## Widens a 32-bit register into a 64-bit one, keeping the sign. + assembler.prefix(Word64, destination, source) + assembler.emit(0x63) + assembler.directOperand(destination, source) + +proc shiftRightImmediate*(assembler: var Assembler, width: Width, + target: Register, count: int) {.raises: [BasicError].} = + ## Shifts right, keeping the sign, by a constant. + if count < 0 or count > 63: + fail("assembler shift count is out of range") + assembler.prefix(width, Register(7), target) + assembler.emit(0xC1) + assembler.directOperand(Register(7), target) + assembler.emit(byte(count)) + +proc storeByteLow*(assembler: var Assembler, base: Register, offset: int, + source: Register) {.raises: [BasicError].} = + ## Stores the low byte of a register through a base register. + ## The REX prefix is forced so the low byte is named, not the high one. + assembler.emit(byte(0x40'u32 or ((source.number shr 3) shl 2) or + (base.number shr 3))) + assembler.emit(0x88) + assembler.memoryOperand(source, base, offset) + +proc offsetOf*(assembler: Assembler, target: Label): int {.raises: [].} = + ## Returns where a label ended up, in bytes. + assembler.targets[int(target)] + +## Logic and indirect control flow + +proc logical(assembler: var Assembler, opcode: byte, width: Width, + destination, source: Register) {.raises: [].} = + ## Encodes one register-to-register logical instruction. + assembler.prefix(width, source, destination) + assembler.emit(opcode) + assembler.directOperand(source, destination) + +proc andRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Keeps the bits both registers hold. + assembler.logical(0x21, width, destination, source) + +proc orRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Keeps the bits either register holds. + assembler.logical(0x09, width, destination, source) + +proc xorRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Keeps the bits exactly one register holds. + assembler.logical(0x31, width, destination, source) + +proc notRegister*(assembler: var Assembler, width: Width, + target: Register) {.raises: [].} = + ## Flips every bit of a register. + assembler.prefix(width, Register(2), target) + assembler.emit(0xF7) + assembler.directOperand(Register(2), target) + +proc callLabel*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Calls a label, pushing the return address. + assembler.emit(0xE8) + assembler.fixups.add( + Fixup(at: assembler.code.len, next: assembler.code.len + 4, + label: int(target)) + ) + assembler.emitDouble(0) + +proc callRegister*(assembler: var Assembler, target: Register) + {.raises: [].} = + ## Calls the address held in a register. + assembler.prefix(Word32, Register(2), target) + assembler.emit(0xFF) + assembler.directOperand(Register(2), target) + +proc jumpRegister*(assembler: var Assembler, target: Register) + {.raises: [].} = + ## Jumps to the address held in a register. + assembler.prefix(Word32, Register(4), target) + assembler.emit(0xFF) + assembler.directOperand(Register(4), target) + +proc testImmediate*(assembler: var Assembler, width: Width, + target: Register, value: int32) {.raises: [].} = + ## Sets flags from a register masked by a constant, keeping neither. + assembler.prefix(width, Register(0), target) + assembler.emit(0xF7) + assembler.directOperand(Register(0), target) + assembler.emitDouble(value) diff --git a/src/bassy/amd64jit.nim b/src/bassy/amd64jit.nim new file mode 100644 index 0000000..bcd06dc --- /dev/null +++ b/src/bassy/amd64jit.nim @@ -0,0 +1,1013 @@ +## The x86-64 half of the code generator, included by jit.nim when the +## target is amd64. It supplies the emitters the walker there calls. +## +## r15 context r12 instruction budget r13 work budget +## rbx globals rbp current frame +## rax rcx rsi rdi r8 r9 r10 working registers; r11 one cell; +## rdx the divide's high half and a spare +## +## The six long-lived registers are the ones both platform conventions +## keep across a call. Windows also keeps rsi and rdi, so those are +## saved on the way in there. Everything else the program touches +## rarely is read from the context when it is needed. + +const + Context = r15 + Instructions = r12 + Work = r13 + GlobalsBase = rbx + RegistersBase = rbp + Temps = [rax, rcx, rsi, rdi, r8, r9, r10] + Cell = r11 + Spare = rdx + +when defined(windows): + const + FirstArgument = rcx + SecondArgument = rdx + Saved = [rbx, rbp, r12, r13, r14, r15, rsi, rdi] + ## Four shadow slots for the callee, plus eight to realign. + Padding = 40 +else: + const + FirstArgument = rdi + SecondArgument = rsi + Saved = [rbx, rbp, r12, r13, r14, r15] + Padding = 8 + +proc temp(index: int): Register {.inline, raises: [].} = + ## Returns one working register. + Temps[index] + +proc nativeCondition(check: Check): Condition {.raises: [].} = + ## Maps a neutral outcome onto the architecture's encoding. + case check + of EqualCheck: EqualCondition + of NotEqualCheck: NotEqualCondition + of LessCheck: LessCondition + of LessEqualCheck: LessEqualCondition + of GreaterCheck: GreaterCondition + of GreaterEqualCheck: GreaterEqualCondition + +type + Emitter = object + ## The assembler. Every branch here reaches anywhere already, so + ## there is nothing to widen. + code: Assembler + far: bool + limit: int + outside: Label + +proc label(e: var Emitter): Label {.inline, raises: [].} = + ## Reserves a label. + e.code.label() + +proc place(e: var Emitter, target: Label) {.inline, raises: [].} = + ## Places a label here. + e.code.place(target) + +proc jump(e: var Emitter, target: Label) {.raises: [].} = + ## Jumps unconditionally. + e.code.branch(target) + +proc jumpWhen(e: var Emitter, condition: Condition, target: Label) + {.raises: [].} = + ## Jumps when a condition holds. + e.code.branchIf(condition, target) + +proc withinProgram(e: var Emitter, offset: Register) {.raises: [].} = + ## Sends an offset that is not one of the program's own to the block + ## past the end, which refuses it, instead of reading a table entry + ## that is not there. One unsigned comparison covers both ends. + e.code.compareImmediate(Word32, offset, int32(e.limit)) + e.jumpWhen(AboveEqualCondition, e.outside) + +proc contextField(e: var Emitter, destination: Register, offset: int) + {.raises: [BasicError].} = + ## Loads one pointer from the context. + e.code.loadDouble(destination, Context, offset) + +proc reach(e: var Emitter, place: Place): (Register, int) + {.raises: [BasicError].} = + ## Returns a base register and byte offset for a value. Displacements + ## are thirty-two bits wide here, so every index is reached directly. + let offset = int(place.index) * ValueStride + case place.home + of SlotHome: (RegistersBase, offset) + of GlobalHome: (GlobalsBase, offset) + of ArgumentHome: + e.contextField(Cell, ContextArguments) + (Cell, offset) + of HostHome: + e.contextField(Cell, ContextHostData) + (Cell, offset) + of CellHome: (Cell, 0) + +proc readValue(e: var Emitter, value, tag: int, place: Place) + {.raises: [BasicError].} = + ## Reads a value's kind and its 32-bit payload. + let (base, offset) = e.reach(place) + e.code.loadByteZeroed(temp(tag), base, offset) + e.code.loadWord(temp(value), base, offset + ValuePayload) + +proc writeWhole(e: var Emitter, place: Place, value: int) + {.raises: [BasicError].} = + ## Writes a whole number. + let (base, offset) = e.reach(place) + e.code.storeByteImmediate(base, offset, 0) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeKind(e: var Emitter, place: Place, tag, value: int) + {.raises: [BasicError].} = + ## Writes a payload under the kind held in a working register. + let (base, offset) = e.reach(place) + e.code.storeByteLow(base, offset, temp(tag)) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeFixed(e: var Emitter, place: Place, value: int) + {.raises: [BasicError].} = + ## Writes a fixed-point payload. + let (base, offset) = e.reach(place) + e.code.storeByteImmediate(base, offset, byte(FixedTag)) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeConstant(e: var Emitter, place: Place, tag: int, bits: int32) + {.raises: [BasicError].} = + ## Writes a constant of a known kind. + let (base, offset) = e.reach(place) + e.code.storeByteImmediate(base, offset, byte(tag)) + e.code.storeWordImmediate(base, offset + ValuePayload, bits) + +proc copyValue(e: var Emitter, destination, source: Place) + {.raises: [BasicError].} = + ## Copies a value entire, whatever kind it holds, as the interpreter + ## does. + let (fromBase, fromOffset) = e.reach(source) + e.code.loadDouble(temp(5), fromBase, fromOffset) + e.code.loadDouble(temp(6), fromBase, fromOffset + ValuePayload) + let (toBase, toOffset) = e.reach(destination) + e.code.storeDouble(temp(5), toBase, toOffset) + e.code.storeDouble(temp(6), toBase, toOffset + ValuePayload) + +proc unlessWhole(e: var Emitter, tag: int, slow: Label) + {.raises: [].} = + ## Takes the slow path unless a kind says whole number. + e.code.testRegister(Word32, temp(tag), temp(tag)) + e.jumpWhen(NotEqualCondition, slow) + +proc unlessNumeric(e: var Emitter, tag: int, slow: Label) + {.raises: [].} = + ## Takes the slow path unless a kind says number of either sort. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.jumpWhen(AboveCondition, slow) + +proc unlessSame(e: var Emitter, tag, other: int, slow: Label) + {.raises: [].} = + ## Takes the slow path unless two kinds agree. + e.code.compareRegister(Word32, temp(tag), temp(other)) + e.jumpWhen(NotEqualCondition, slow) + +proc jumpIfSame(e: var Emitter, tag, other: int, target: Label) + {.raises: [].} = + ## Jumps when two kinds agree. + e.code.compareRegister(Word32, temp(tag), temp(other)) + e.jumpWhen(EqualCondition, target) + +proc whenWhole(e: var Emitter, tag: int, target: Label) {.raises: [].} = + ## Jumps when a kind says whole number. + e.code.testRegister(Word32, temp(tag), temp(tag)) + e.jumpWhen(EqualCondition, target) + +proc unlessFixed(e: var Emitter, tag: int, slow: Label) {.raises: [].} = + ## Takes the slow path unless a kind says fixed point. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.jumpWhen(NotEqualCondition, slow) + +proc toFixed(e: var Emitter, value: int, slow: Label) + {.raises: [BasicError].} = + ## Turns a whole number into Q16.16 bits. One outside the fixed-point + ## range cannot be, which the interpreter refuses, so that goes slow. + let register = temp(value) + e.code.compareImmediate(Word32, register, 32767) + e.jumpWhen(GreaterCondition, slow) + e.code.compareImmediate(Word32, register, -32768) + e.jumpWhen(LessCondition, slow) + e.code.shiftLeftImmediate(Word32, register, FixedShift) + +proc scaleWide(e: var Emitter, value, tag: int) {.raises: [BasicError].} = + ## Widens a number of either kind to sixty-four bits on the fixed-point + ## scale, where every whole number and every fixed-point one compare + ## exactly, as the interpreter compares them. + let register = temp(value) + let done = e.label() + e.code.signExtendDouble(register, register) + e.code.testRegister(Word32, temp(tag), temp(tag)) + e.jumpWhen(NotEqualCondition, done) + e.code.shiftLeftImmediate(Word64, register, FixedShift) + e.place(done) + +proc loadWide(e: var Emitter, value: int, bits: int64) {.raises: [].} = + ## Loads a sixty-four bit constant into a working register. + e.code.loadImmediate(Word64, temp(value), bits) + +proc compareWide(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two widened working registers. + e.code.compareRegister(Word64, temp(left), temp(right)) + +proc whenFixed(e: var Emitter, tag: int, target: Label) + {.raises: [].} = + ## Jumps when a kind says fixed point. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.jumpWhen(EqualCondition, target) + +proc loadConstant(e: var Emitter, value: int, bits: int32) + {.raises: [].} = + ## Loads a constant into a working register. + e.code.loadImmediate(Word32, temp(value), int64(bits)) + +proc add(e: var Emitter, left, right: int) {.raises: [].} = + ## Adds, wrapping. + e.code.addRegister(Word32, temp(left), temp(right)) + +proc subtract(e: var Emitter, left, right: int) {.raises: [].} = + ## Subtracts, wrapping. + e.code.subtractRegister(Word32, temp(left), temp(right)) + +proc multiply(e: var Emitter, left, right: int) {.raises: [].} = + ## Multiplies, wrapping. + e.code.multiplyRegister(Word32, temp(left), temp(right)) + +proc negate(e: var Emitter, value: int) {.raises: [].} = + ## Negates, wrapping. + e.code.negateRegister(Word32, temp(value)) + +proc bitAnd(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits both hold. + e.code.andRegister(Word32, temp(left), temp(right)) + +proc bitOr(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits either holds. + e.code.orRegister(Word32, temp(left), temp(right)) + +proc bitXor(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits exactly one holds. + e.code.xorRegister(Word32, temp(left), temp(right)) + +proc bitNot(e: var Emitter, value: int) {.raises: [].} = + ## Flips every bit. + e.code.notRegister(Word32, temp(value)) + +proc divide(e: var Emitter, left, right: int, keepRemainder: bool) + {.raises: [].} = + ## Divides toward zero; the divisor is known not to be zero. The + ## hardware traps on the most negative number over minus one, which + ## the interpreter defines, so minus one is answered without dividing: + ## the quotient is the negation, wrapping, and nothing is left over. + let normal = e.label() + let done = e.label() + e.code.compareImmediate(Word32, temp(right), -1) + e.jumpWhen(NotEqualCondition, normal) + if keepRemainder: + e.code.loadImmediate(Word32, temp(left), 0) + else: + e.code.negateRegister(Word32, temp(left)) + e.jump(done) + e.place(normal) + e.code.moveRegister(Word32, rax, temp(left)) + e.code.signExtendToPair(Word32) + e.code.signedDivide(Word32, temp(right)) + if keepRemainder: + e.code.moveRegister(Word32, temp(left), Spare) + else: + e.code.moveRegister(Word32, temp(left), rax) + e.place(done) + +proc quotient(e: var Emitter, left, right: int) {.raises: [].} = + ## Divides toward zero. + e.divide(left, right, false) + +proc remainder(e: var Emitter, left, right: int) + {.raises: [].} = + ## Leaves what dividing left over, with the sign of the dividend. + e.divide(left, right, true) + +proc multiplyFixed(e: var Emitter, left, right: int) + {.raises: [BasicError].} = + ## Multiplies two Q16.16 numbers through a widened intermediate, + ## rounding to nearest exactly as the fixed-point library does. + e.code.signExtendDouble(temp(left), temp(left)) + e.code.signExtendDouble(temp(6), temp(right)) + e.code.multiplyRegister(Word64, temp(left), temp(6)) + e.code.addImmediate(Word64, temp(left), int32(FixedRounding)) + e.code.shiftRightImmediate(Word64, temp(left), FixedShift) + e.code.moveRegister(Word32, temp(left), temp(left)) + +proc widenToFixed(e: var Emitter, value, tag: int, slow: Label) + {.raises: [BasicError].} = + ## Turns a number of either kind into its Q16.16 bits, widened to + ## sixty-four. A whole number outside the fixed-point range cannot + ## become one, which the interpreter refuses, so that goes slow. + let register = temp(value) + let already = e.label() + let ready = e.label() + e.whenFixed(tag, already) + e.code.compareImmediate(Word32, register, 32767) + e.jumpWhen(GreaterCondition, slow) + e.code.compareImmediate(Word32, register, -32768) + e.jumpWhen(LessCondition, slow) + e.code.signExtendDouble(register, register) + e.code.shiftLeftImmediate(Word64, register, FixedShift) + e.jump(ready) + e.place(already) + e.code.signExtendDouble(register, register) + e.place(ready) + +proc divideFixed(e: var Emitter, left, right: int, slow: Label) + {.raises: [BasicError].} = + ## Divides two widened Q16.16 numbers, rounding to nearest with halves + ## going up, for either sign, exactly as the fixed-point library + ## does: the signs are put right first, half the divisor is added, + ## and the truncating divide is corrected back to a floor. The divide + ## works in rax and rdx, so the numerator is moved there. + let denominator = temp(right) + let half = temp(5) + let numerator = temp(6) + e.code.moveRegister(Word64, rax, temp(left)) + e.code.compareImmediate(Word64, denominator, 0) + e.jumpWhen(EqualCondition, slow) + let signsSettled = e.label() + e.jumpWhen(GreaterCondition, signsSettled) + e.code.negateRegister(Word64, rax) + e.code.negateRegister(Word64, denominator) + e.place(signsSettled) + e.code.shiftLeftImmediate(Word64, rax, FixedShift) + e.code.moveRegister(Word64, half, denominator) + e.code.shiftRightImmediate(Word64, half, 1) + e.code.addRegister(Word64, rax, half) + e.code.moveRegister(Word64, numerator, rax) + e.code.signExtendToPair(Word64) + e.code.signedDivide(Word64, denominator) + let done = e.label() + e.code.testRegister(Word64, Spare, Spare) + e.jumpWhen(EqualCondition, done) + e.code.testRegister(Word64, numerator, numerator) + e.jumpWhen(GreaterEqualCondition, done) + e.code.subtractImmediate(Word64, rax, 1) + e.place(done) + e.code.moveRegister(Word32, temp(left), rax) + +proc compare(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two working registers. + e.code.compareRegister(Word32, temp(left), temp(right)) + +proc compareConstant(e: var Emitter, value: int, bits: int32) + {.raises: [].} = + ## Sets flags from a working register against a constant. + e.code.compareImmediate(Word32, temp(value), bits) + +proc answer(e: var Emitter, value: int, check: Check) {.raises: [].} = + ## Writes BASIC's -1 for true and zero for false. The byte form only + ## names the low byte of rax, rcx, rdx and rbx without a prefix, so + ## this is only ever asked of the first working register. + e.code.setIfCondition(temp(value), nativeCondition(check)) + e.code.negateRegister(Word32, temp(value)) + +proc jumpOn(e: var Emitter, check: Check, target: Label) + {.raises: [].} = + ## Jumps on a comparison outcome. + e.jumpWhen(nativeCondition(check), target) + +proc jumpIfZeroValue(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a working register holds zero. + e.code.testRegister(Word32, temp(value), temp(value)) + e.jumpWhen(EqualCondition, target) + +proc jumpIfNotZeroValue(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a working register holds anything but zero. + e.code.testRegister(Word32, temp(value), temp(value)) + e.jumpWhen(NotEqualCondition, target) + +proc jumpIfLowBits(e: var Emitter, value, bits: int, target: Label) + {.raises: [].} = + ## Jumps when any of a working register's lowest bits is set. + e.code.testImmediate(Word32, temp(value), int32((1'i64 shl bits) - 1)) + e.jumpWhen(NotEqualCondition, target) + +proc cellAddress(e: var Emitter, index: int, extent: ArrayExtent, + slow: Label) {.raises: [BasicError].} = + ## Bounds checks an index and leaves the cell's address in Cell. One + ## unsigned comparison covers both ends, as the interpreter's does. + ## The cells' base is read from the context rather than kept in a + ## register, which leaves one more register for a loop's globals. The + ## index register is left scaled. + e.code.compareImmediate(Word32, temp(index), extent.length) + e.jumpWhen(AboveEqualCondition, slow) + e.code.addImmediate(Word32, temp(index), extent.base) + e.code.shiftLeftImmediate(Word64, temp(index), 4) + e.contextField(Cell, ContextMemory) + e.code.addRegister(Word64, Cell, temp(index)) + +proc charge(e: var Emitter, instructions, work: int32) {.raises: [].} = + ## Charges both budgets, already known to cover it, without looking. + e.code.subtractImmediate(Word64, Instructions, instructions) + e.code.subtractImmediate(Word64, Work, work) + +proc meter(e: var Emitter, instructions, work: int32, slow: Label, + needInstructions = int64(instructions), needWork = int64(work)) + {.raises: [].} = + ## Checks both budgets hold what is needed before charging either, as + ## the interpreter does. What is needed can be more than this block + ## costs, when one look is to cover every block until the next. + for (budget, need) in [(Instructions, needInstructions), + (Work, needWork)]: + if need <= int64(high(int32)): + e.code.compareImmediate(Word64, budget, int32(need)) + else: + e.code.loadImmediate(Word64, Spare, need) + e.code.compareRegister(Word64, budget, Spare) + e.jumpWhen(LessCondition, slow) + e.charge(instructions, work) + +proc slotAddress(e: var Emitter, destination, index: Register) + {.raises: [BasicError].} = + ## Points a register at one register-file slot by its absolute index. + ## The index register is left scaled. + e.contextField(destination, ContextRegisterFile) + e.code.shiftLeftImmediate(Word64, index, 4) + e.code.addRegister(Word64, destination, index) + +proc copyValues(e: var Emitter, destination, source: Register, + count: int) {.raises: [BasicError].} = + ## Copies a run of whole values, in a loop once there are many. Works + ## in rax, rdx, r8, r9 and r10, so neither end may be one of those. + if count <= 8: + for index in 0 ..< count: + e.code.loadDouble(rax, source, index * ValueStride) + e.code.loadDouble(Spare, source, index * ValueStride + ValuePayload) + e.code.storeDouble(rax, destination, index * ValueStride) + e.code.storeDouble(Spare, destination, + index * ValueStride + ValuePayload) + return + e.code.moveRegister(Word64, r10, source) + e.code.moveRegister(Word64, r9, destination) + e.code.loadImmediate(Word32, r8, int64(count)) + let again = e.label() + e.place(again) + e.code.loadDouble(rax, r10, 0) + e.code.loadDouble(Spare, r10, ValuePayload) + e.code.storeDouble(rax, r9, 0) + e.code.storeDouble(Spare, r9, ValuePayload) + e.code.addImmediate(Word64, r10, ValueStride) + e.code.addImmediate(Word64, r9, ValueStride) + e.code.subtractImmediate(Word32, r8, 1) + e.jumpWhen(NotEqualCondition, again) + +proc clearValues(e: var Emitter, destination: Register, count: int) + {.raises: [BasicError].} = + ## Zeroes a run of values, in a loop once there are many. + e.code.loadImmediate(Word32, rax, 0) + if count <= 8: + for index in 0 ..< count: + e.code.storeDouble(rax, destination, index * ValueStride) + e.code.storeDouble(rax, destination, + index * ValueStride + ValuePayload) + return + e.code.moveRegister(Word64, r9, destination) + e.code.loadImmediate(Word32, r8, int64(count)) + let again = e.label() + e.place(again) + e.code.storeDouble(rax, r9, 0) + e.code.storeDouble(rax, r9, ValuePayload) + e.code.addImmediate(Word64, r9, ValueStride) + e.code.subtractImmediate(Word32, r8, 1) + e.jumpWhen(NotEqualCondition, again) + +proc enterRoutine(e: var Emitter, gosub: bool, calleeId: int32, + calleeRegisters, calleeParameters, callerRegisters: int32, + resumeAt: int32, limits: CallLimits, slow: Label) + {.raises: [BasicError].} = + ## Pushes a frame into the interpreter's own array and moves the + ## current frame on, refusing the same two ceilings it refuses. + let depth = rax + let oldBase = rcx + let newBase = rsi + let frame = rdi + e.code.loadWord(depth, Context, ContextDepth) + e.code.compareImmediate(Word32, depth, limits.frames - 1) + e.jumpWhen(GreaterEqualCondition, slow) + e.code.loadWord(oldBase, Context, ContextBase) + e.code.moveRegister(Word32, newBase, oldBase) + e.code.addImmediate(Word32, newBase, callerRegisters) + e.code.compareImmediate(Word32, newBase, + limits.slots - calleeRegisters) + e.jumpWhen(GreaterCondition, slow) + + e.contextField(frame, ContextFrames) + e.code.moveRegister(Word32, r8, depth) + e.code.shiftLeftImmediate(Word64, r8, 4) + e.code.addRegister(Word64, frame, r8) + e.code.storeWord(oldBase, frame, FrameBase) + e.code.loadWord(r8, Context, ContextRoutine) + e.code.storeWord(r8, frame, FrameRoutine) + e.code.storeWordImmediate(frame, FrameReturn, resumeAt) + e.code.storeWordImmediate(frame, FrameTag, if gosub: 1 else: 0) + + e.code.addImmediate(Word32, depth, 1) + e.code.storeWord(depth, Context, ContextDepth) + e.code.storeWord(newBase, Context, ContextBase) + e.code.storeWordImmediate(Context, ContextRoutine, calleeId) + + # A GOSUB hands the callee a copy of the caller's slots; a call clears + # them and lays the arguments over the first few, in that order. + e.code.moveRegister(Word64, frame, RegistersBase) + e.code.moveRegister(Word32, rcx, newBase) + e.slotAddress(RegistersBase, rcx) + if gosub: + e.copyValues(RegistersBase, frame, int(calleeRegisters)) + else: + e.clearValues(RegistersBase, int(calleeRegisters)) + if calleeParameters > 0: + e.contextField(Cell, ContextArguments) + e.copyValues(RegistersBase, Cell, int(calleeParameters)) + +proc leaveRoutine(e: var Emitter, parameters: int32, exitSub: bool, + slow: Label) {.raises: [BasicError].} = + ## Pops a frame and jumps to wherever it said to carry on. A GOSUB + ## frame first hands the shared parameters back to the caller. Leaving + ## a sub outright only goes this way when its own frame is on top. + ## Nothing is written until every refusal has been passed, the offset + ## it would carry on at included, so a refusal leaves the frame on. + let depth = rax + let frame = rcx + let base = rsi + e.code.loadWord(depth, Context, ContextDepth) + e.code.testRegister(Word32, depth, depth) + e.jumpWhen(EqualCondition, slow) + e.code.subtractImmediate(Word32, depth, 1) + e.contextField(frame, ContextFrames) + e.code.moveRegister(Word32, r8, depth) + e.code.shiftLeftImmediate(Word64, r8, 4) + e.code.addRegister(Word64, frame, r8) + if exitSub: + e.code.loadByteZeroed(Spare, frame, FrameTag) + e.code.testRegister(Word32, Spare, Spare) + e.jumpWhen(NotEqualCondition, slow) + e.code.loadWord(Cell, frame, FrameReturn) + e.withinProgram(Cell) + e.code.storeWord(depth, Context, ContextDepth) + e.code.loadWord(base, frame, FrameBase) + if parameters > 0: + let plain = e.label() + e.code.loadByteZeroed(Spare, frame, FrameTag) + e.code.compareImmediate(Word32, Spare, 1) + e.jumpWhen(NotEqualCondition, plain) + e.code.moveRegister(Word32, r8, base) + e.slotAddress(rdi, r8) + e.copyValues(rdi, RegistersBase, int(parameters)) + e.place(plain) + e.code.storeWord(base, Context, ContextBase) + e.code.loadWord(Spare, frame, FrameRoutine) + e.code.storeWord(Spare, Context, ContextRoutine) + e.code.loadWord(Spare, frame, FrameReturn) + e.code.storeWord(Spare, Context, ContextOffset) + e.code.moveRegister(Word32, r8, base) + e.slotAddress(RegistersBase, r8) + e.contextField(rax, ContextTable) + e.code.shiftLeftImmediate(Word64, Spare, 3) + e.code.addRegister(Word64, rax, Spare) + e.code.loadDouble(rax, rax, 0) + e.code.jumpRegister(rax) + +proc callSlow(e: var Emitter, offset: int32, routine: Label) + {.raises: [].} = + ## Runs the interpreter's own code for one instruction. + e.code.loadImmediate(Word32, SecondArgument, int64(offset)) + e.code.callLabel(routine) + +proc slowRoutine(e: var Emitter, failed: Label, helper: int) + {.raises: [BasicError].} = + ## The one place compiled code calls out. The budgets go into the + ## context for the interpreter's code to charge, and come back from it + ## along with the frame, since a call or a return may have moved it. + ## The globals' base comes back too, host code having possibly replaced + ## that buffer; every other base is read from the context where used. + ## A failure leaves through the shared exit, dropping the return + ## address this routine was called with on the way. + let refused = e.label() + e.code.storeDouble(Instructions, Context, ContextInstructions) + e.code.storeDouble(Work, Context, ContextWork) + e.code.moveRegister(Word64, FirstArgument, Context) + e.code.subtractImmediate(Word64, rsp, Padding) + e.contextField(rax, helper) + e.code.callRegister(rax) + e.code.addImmediate(Word64, rsp, Padding) + e.code.moveRegister(Word32, r10, rax) + e.code.loadDouble(Instructions, Context, ContextInstructions) + e.code.loadDouble(Work, Context, ContextWork) + e.contextField(GlobalsBase, 0) + e.code.loadWord(rcx, Context, ContextBase) + e.slotAddress(RegistersBase, rcx) + e.code.testRegister(Word32, r10, r10) + e.jumpWhen(NotEqualCondition, refused) + e.code.returnToCaller() + e.place(refused) + e.code.addImmediate(Word64, rsp, 8) + e.jump(failed) + +proc dispatch(e: var Emitter) {.raises: [BasicError].} = + ## Jumps to the block for whatever offset the context names. + e.code.loadWord(rax, Context, ContextOffset) + e.withinProgram(rax) + e.code.shiftLeftImmediate(Word64, rax, 3) + e.contextField(rcx, ContextTable) + e.code.addRegister(Word64, rcx, rax) + e.code.loadDouble(rcx, rcx, 0) + e.code.jumpRegister(rcx) + +proc prologue(e: var Emitter) {.raises: [BasicError].} = + ## Saves what the platform says to keep and loads the machine state. + for register in Saved: + e.code.push(register) + e.code.subtractImmediate(Word64, rsp, Padding) + e.code.moveRegister(Word64, Context, FirstArgument) + e.code.loadDouble(GlobalsBase, Context, 0) + e.code.loadDouble(Instructions, Context, ContextInstructions) + e.code.loadDouble(Work, Context, ContextWork) + e.code.loadWord(rcx, Context, ContextBase) + e.slotAddress(RegistersBase, rcx) + +proc restoreAndReturn(e: var Emitter) {.raises: [].} = + ## Restores what the platform says to keep and returns rax as it is. + e.code.addImmediate(Word64, rsp, Padding) + for index in countdown(Saved.len - 1, 0): + e.code.pop(Saved[index]) + e.code.returnToCaller() + +proc epilogue(e: var Emitter, status: NativeStatus) + {.raises: [].} = + ## Restores what the platform says to keep and returns a status. + e.code.loadImmediate(Word32, rax, int64(ord(status))) + e.restoreAndReturn() + +proc leaveWithAnswer(e: var Emitter) {.raises: [].} = + ## Returns whatever status the interpreter's code answered with. + e.code.moveRegister(Word32, rax, r10) + e.restoreAndReturn() + +## Globals held in registers +## +## Inside a specialised loop its globals live in the registers below, +## proved whole numbers on the way in. The globals' own base register is +## one of them, so the globals are reached through Cell while a loop +## runs, and the base is read back from the context on the way out. + +const Hoisting* = [r8, r14, rbx] + +proc hoisted(slot: int): Register {.inline, raises: [].} = + ## Returns the register holding one hoisted global. + Hoisting[slot] + +proc beginHoisting(e: var Emitter) {.raises: [BasicError].} = + ## Reaches the globals through Cell, which no hoisted value occupies. + e.contextField(Cell, 0) + +proc endHoisting(e: var Emitter) {.raises: [BasicError].} = + ## Puts the globals' base back where the general code expects it. + e.contextField(GlobalsBase, 0) + +proc guardHoisted(e: var Emitter, index: int32, failed: Label) + {.raises: [BasicError].} = + ## Leaves for the general code unless a global holds a whole number. + let offset = int(index) * ValueStride + e.code.loadByteZeroed(temp(0), Cell, offset) + e.code.testRegister(Word32, temp(0), temp(0)) + e.jumpWhen(NotEqualCondition, failed) + +proc loadHoisted(e: var Emitter, slot: int, index: int32) + {.raises: [BasicError].} = + ## Reads one global's payload into its register. + e.code.loadWord(hoisted(slot), Cell, + int(index) * ValueStride + ValuePayload) + +proc storeHoisted(e: var Emitter, slot: int, index: int32) + {.raises: [BasicError].} = + ## Publishes one register back as a whole number. + let offset = int(index) * ValueStride + e.code.storeByteImmediate(Cell, offset, 0) + e.code.storeWord(hoisted(slot), Cell, offset + ValuePayload) + +proc setHoisted(e: var Emitter, slot: int, bits: int32) {.raises: [].} = + ## Loads a constant into a hoisted global. + e.code.loadImmediate(Word32, hoisted(slot), int64(bits)) + +proc copyHoisted(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Copies one hoisted global into another. + e.code.moveRegister(Word32, hoisted(destination), hoisted(source)) + +proc addHoisted(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Adds one hoisted global into another, wrapping. + e.code.addRegister(Word32, hoisted(destination), hoisted(source)) + +proc addHoistedConstant(e: var Emitter, slot: int, bits: int32) + {.raises: [].} = + ## Adds a constant to a hoisted global, wrapping. + e.code.addImmediate(Word32, hoisted(slot), bits) + +proc hoistedToTemp(e: var Emitter, value, slot: int) {.raises: [].} = + ## Copies a hoisted global into a working register. + e.code.moveRegister(Word32, temp(value), hoisted(slot)) + +proc tempToHoisted(e: var Emitter, slot, value: int) {.raises: [].} = + ## Copies a working register into a hoisted global. + e.code.moveRegister(Word32, hoisted(slot), temp(value)) + +proc addTempToHoisted(e: var Emitter, slot, value: int) {.raises: [].} = + ## Adds a working register into a hoisted global, wrapping. + e.code.addRegister(Word32, hoisted(slot), temp(value)) + +proc compareHoisted(e: var Emitter, slot: int, bits: int32) + {.raises: [].} = + ## Sets flags from a hoisted global against a constant. + e.code.compareImmediate(Word32, hoisted(slot), bits) + +## Values held in registers across a block +## +## A block's fast version keeps the slots and globals it touches in the +## registers below. Nothing in the pool outlives the block: every held +## value is written back before anything else could look at memory. rax +## and rdx stay out of the pool for the divide and as scratch. + +const + Pool* = [rcx, rsi, rdi, r8, r9, r10, r14] + FastScratch = rax + +proc pooled(index: int): Register {.inline, raises: [].} = + ## Returns one pool register. + Pool[index] + +proc fastLoad(e: var Emitter, payload, tag: int, place: Place, + deopt: Label) {.raises: [BasicError].} = + ## Reads a value's kind and payload, taking the deopt path unless it + ## is a number of either kind. + let (base, offset) = e.reach(place) + e.code.loadByteZeroed(pooled(tag), base, offset) + e.code.loadWord(pooled(payload), base, offset + ValuePayload) + e.code.compareImmediate(Word32, pooled(tag), FixedTag) + e.jumpWhen(AboveCondition, deopt) + +proc fastStore(e: var Emitter, place: Place, payload, tag, kind: int) + {.raises: [BasicError].} = + ## Writes a value back: its kind from a register, or the known kind. + let (base, offset) = e.reach(place) + if tag >= 0: + e.code.storeByteLow(base, offset, pooled(tag)) + else: + e.code.storeByteImmediate(base, offset, byte(kind)) + e.code.storeWord(pooled(payload), base, offset + ValuePayload) + +proc fastMove(e: var Emitter, destination, source: int) {.raises: [].} = + ## Copies one pool register into another. + e.code.moveRegister(Word32, pooled(destination), pooled(source)) + +proc fastConstant(e: var Emitter, destination: int, bits: int32) + {.raises: [].} = + ## Loads a constant into a pool register. + e.code.loadImmediate(Word32, pooled(destination), int64(bits)) + +proc fastAdd(e: var Emitter, destination, source: int) {.raises: [].} = + ## Adds, wrapping. + e.code.addRegister(Word32, pooled(destination), pooled(source)) + +proc fastSubtract(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Subtracts, wrapping. + e.code.subtractRegister(Word32, pooled(destination), pooled(source)) + +proc fastMultiply(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Multiplies, wrapping. + e.code.multiplyRegister(Word32, pooled(destination), pooled(source)) + +proc fastMultiplyFixed(e: var Emitter, destination, source: int) + {.raises: [BasicError].} = + ## Multiplies two Q16.16 numbers, rounding as the library does. + let target = pooled(destination) + e.code.signExtendDouble(target, target) + e.code.signExtendDouble(FastScratch, pooled(source)) + e.code.multiplyRegister(Word64, target, FastScratch) + e.code.addImmediate(Word64, target, int32(FixedRounding)) + e.code.shiftRightImmediate(Word64, target, FixedShift) + e.code.moveRegister(Word32, target, target) + +proc fastNegate(e: var Emitter, destination: int) {.raises: [].} = + ## Negates, wrapping. + e.code.negateRegister(Word32, pooled(destination)) + +proc fastToFixed(e: var Emitter, destination: int, deopt: Label) + {.raises: [BasicError].} = + ## Turns a whole number into Q16.16 bits, or takes the deopt path when + ## it is outside the fixed-point range, which the interpreter refuses. + let target = pooled(destination) + e.code.compareImmediate(Word32, target, 32767) + e.jumpWhen(GreaterCondition, deopt) + e.code.compareImmediate(Word32, target, -32768) + e.jumpWhen(LessCondition, deopt) + e.code.shiftLeftImmediate(Word32, target, FixedShift) + +proc fastCompare(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two pool registers. + e.code.compareRegister(Word32, pooled(left), pooled(right)) + +proc fastCompareWide(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two widened pool registers. + e.code.compareRegister(Word64, pooled(left), pooled(right)) + +proc fastCompareConstant(e: var Emitter, value: int, bits: int32) + {.raises: [].} = + ## Sets flags from a pool register against a constant. + e.code.compareImmediate(Word32, pooled(value), bits) + +proc fastLoadWide(e: var Emitter, destination: int, bits: int64) + {.raises: [].} = + ## Loads a sixty-four bit constant into a pool register. + e.code.loadImmediate(Word64, pooled(destination), bits) + +proc fastScaleWide(e: var Emitter, value, tag: int) + {.raises: [BasicError].} = + ## Widens a number to sixty-four bits on the fixed-point scale, its + ## kind read from a register. + let target = pooled(value) + let done = e.label() + e.code.signExtendDouble(target, target) + e.code.testRegister(Word32, pooled(tag), pooled(tag)) + e.jumpWhen(NotEqualCondition, done) + e.code.shiftLeftImmediate(Word64, target, FixedShift) + e.place(done) + +proc fastAnswer(e: var Emitter, destination: int, check: Check) + {.raises: [].} = + ## Writes BASIC's -1 for true and zero for false. + e.code.setIfCondition(pooled(destination), nativeCondition(check)) + e.code.negateRegister(Word32, pooled(destination)) + +proc fastJumpIfZero(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a pool register holds zero. + e.code.testRegister(Word32, pooled(value), pooled(value)) + e.jumpWhen(EqualCondition, target) + +proc fastJumpIfNotZero(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a pool register holds anything but zero. + e.code.testRegister(Word32, pooled(value), pooled(value)) + e.jumpWhen(NotEqualCondition, target) + +proc fastJumpIfDiffer(e: var Emitter, left, right: int, target: Label) + {.raises: [].} = + ## Jumps when two pool registers differ. + e.code.compareRegister(Word32, pooled(left), pooled(right)) + e.jumpWhen(NotEqualCondition, target) + +proc fastCellAddress(e: var Emitter, index: int, extent: ArrayExtent, + deopt: Label) {.raises: [BasicError].} = + ## Bounds checks an index held in the pool, leaving it untouched, and + ## leaves the cell's address in Cell. + let position = pooled(index) + e.code.compareImmediate(Word32, position, extent.length) + e.jumpWhen(AboveEqualCondition, deopt) + e.code.moveRegister(Word32, FastScratch, position) + e.code.addImmediate(Word32, FastScratch, extent.base) + e.code.shiftLeftImmediate(Word64, FastScratch, 4) + e.contextField(Cell, ContextMemory) + e.code.addRegister(Word64, Cell, FastScratch) + +## Strings read in place +## +## A string value is its storage's owner in the high half and a handle +## into the span table in the low half. Everything is checked just as +## the interpreter checks it, and anything amiss takes the slow path, +## which raises the interpreter's own error. + +proc stringSpan(e: var Emitter, place: Place, reference, start, + length: Register, slow: Label) {.raises: [BasicError].} = + ## Reads a string's start and length, or takes the slow path unless + ## the value is a string of this storage's current generation. + let (base, offset) = e.reach(place) + e.code.loadByteZeroed(start, base, offset) + e.code.compareImmediate(Word32, start, StringTag) + e.jumpWhen(NotEqualCondition, slow) + e.code.loadDouble(reference, base, offset + ValuePayload) + e.contextField(length, ContextStringOwner) + e.code.loadWord(length, length, 0) + e.code.testRegister(Word32, length, length) + e.jumpWhen(EqualCondition, slow) + e.code.moveRegister(Word64, start, reference) + e.code.shiftRightImmediate(Word64, start, 32) + e.code.compareRegister(Word32, start, length) + e.jumpWhen(NotEqualCondition, slow) + e.contextField(length, ContextStringSpans) + e.code.loadDouble(start, length, 0) + e.code.moveRegister(Word32, reference, reference) + e.code.compareRegister(Word64, reference, start) + e.jumpWhen(AboveEqualCondition, slow) + e.code.loadDouble(length, length, 8) + e.code.moveRegister(Word64, start, reference) + e.code.shiftLeftImmediate(Word64, start, 3) + e.code.addRegister(Word64, length, start) + e.code.loadWord(start, length, 8) + e.code.loadWord(length, length, 12) + +proc chargeWork(e: var Emitter, cost: Register, slow: Label) + {.raises: [].} = + ## Charges work worked out at run time, or takes the slow path when it + ## cannot be afforded, where the interpreter raises. + e.code.compareRegister(Word64, Work, cost) + e.jumpWhen(LessCondition, slow) + e.code.subtractRegister(Word64, Work, cost) + +proc arenaBase(e: var Emitter, destination: Register) + {.raises: [BasicError].} = + ## Points at the first byte of the string arena. + e.contextField(destination, ContextStringArena) + e.code.loadDouble(destination, destination, 8) + e.code.addImmediate(Word64, destination, 8) + +proc stringFunction(e: var Emitter, function: TextFunction, slow: Label) + {.raises: [BasicError].} = + ## Answers LEN or ASC of the first argument into the first working + ## register, charging what the interpreter charges. + e.stringSpan(argument(0), r8, rsi, rdi, slow) + if function == CodeFunction: + e.code.testRegister(Word32, rdi, rdi) + e.jumpWhen(EqualCondition, slow) + e.code.moveRegister(Word32, Spare, rdi) + e.code.addImmediate(Word64, Spare, 1) + e.chargeWork(Spare, slow) + if function == LengthFunction: + e.code.moveRegister(Word32, temp(0), rdi) + else: + e.arenaBase(Cell) + e.code.addRegister(Word64, Cell, rsi) + e.code.loadByteZeroed(temp(0), Cell, 0) + +proc stringEquality(e: var Emitter, left, right: Place, equal: bool, + slow: Label) {.raises: [BasicError].} = + ## Answers whether two strings hold the same bytes, into the first + ## working register, charging both lengths as the interpreter does. + e.stringSpan(left, r8, rsi, rdi, slow) + e.stringSpan(right, r9, r10, r14, slow) + e.code.moveRegister(Word32, Spare, rdi) + e.code.addRegister(Word64, Spare, r14) + e.chargeWork(Spare, slow) + let differ = e.label() + let same = e.label() + let done = e.label() + e.code.compareRegister(Word32, rdi, r14) + e.jumpWhen(NotEqualCondition, differ) + e.arenaBase(Cell) + e.code.addRegister(Word64, rsi, Cell) + e.code.addRegister(Word64, r10, Cell) + e.code.testRegister(Word32, rdi, rdi) + e.jumpWhen(EqualCondition, same) + let again = e.label() + e.place(again) + e.code.loadByteZeroed(rax, rsi, 0) + e.code.loadByteZeroed(Spare, r10, 0) + e.code.compareRegister(Word32, rax, Spare) + e.jumpWhen(NotEqualCondition, differ) + e.code.addImmediate(Word64, rsi, 1) + e.code.addImmediate(Word64, r10, 1) + e.code.subtractImmediate(Word32, rdi, 1) + e.jumpWhen(NotEqualCondition, again) + e.place(same) + e.code.loadImmediate(Word32, temp(0), if equal: -1 else: 0) + e.jump(done) + e.place(differ) + e.code.loadImmediate(Word32, temp(0), if equal: 0 else: -1) + e.place(done) + +proc jumpUnlessTag(e: var Emitter, tag: int, value: int, target: Label) + {.raises: [].} = + ## Jumps unless a working register holds one particular tag. + e.code.compareImmediate(Word32, temp(tag), int32(value)) + e.jumpWhen(NotEqualCondition, target) + +proc halt(e: var Emitter, offset: int32) {.raises: [BasicError].} = + ## Publishes the budgets and where the program stopped, then returns. + e.code.storeDouble(Instructions, Context, ContextInstructions) + e.code.storeDouble(Work, Context, ContextWork) + e.code.storeWordImmediate(Context, ContextOffset, offset) + e.epilogue(NativeCompleted) + +proc finish(e: var Emitter): seq[byte] {.raises: [BasicError].} = + ## Resolves every branch and returns the finished bytes. + e.code.resolve() + e.code.code + +proc offsetBytes(e: Emitter, target: Label): int {.raises: [].} = + ## Returns where a label ended up, in bytes. + e.code.offsetOf(target) diff --git a/src/bassy/arm64.nim b/src/bassy/arm64.nim new file mode 100644 index 0000000..51409ad --- /dev/null +++ b/src/bassy/arm64.nim @@ -0,0 +1,550 @@ +## Encodes the AArch64 subset the BASIC compiler needs. +## Every instruction is one 32-bit word, so labels patch in place. +## Encodings follow the Arm Architecture Reference Manual field layouts. + +import numbers + +type + Register* = distinct uint32 + + Width* = enum + ## Selects the 32-bit W or 64-bit X view of a register. + Word32, + Word64 + + Condition* = enum + ## Branch and select conditions, in architectural encoding order. + EqualCondition, + NotEqualCondition, + CarrySetCondition, + CarryClearCondition, + NegativeCondition, + PositiveCondition, + OverflowCondition, + NoOverflowCondition, + UnsignedGreaterCondition, + UnsignedLessEqualCondition, + GreaterEqualCondition, + LessCondition, + GreaterCondition, + LessEqualCondition, + AlwaysCondition + + Label* = distinct int + + FixupKind = enum + Branch26Fixup, + Branch19Fixup + + Fixup = object + kind: FixupKind + at: int + label: int + + Assembler* = object + ## Collects instruction words plus unresolved label references. + code*: seq[uint32] + targets: seq[int] + fixups: seq[Fixup] + +const + x0* = Register(0) + x1* = Register(1) + x2* = Register(2) + x3* = Register(3) + x4* = Register(4) + x5* = Register(5) + x6* = Register(6) + x7* = Register(7) + x8* = Register(8) + x9* = Register(9) + x10* = Register(10) + x11* = Register(11) + x12* = Register(12) + x13* = Register(13) + x14* = Register(14) + x15* = Register(15) + x16* = Register(16) + x17* = Register(17) + x19* = Register(19) + x20* = Register(20) + x21* = Register(21) + x22* = Register(22) + x23* = Register(23) + x24* = Register(24) + x25* = Register(25) + x26* = Register(26) + x27* = Register(27) + x28* = Register(28) + framePointer* = Register(29) + linkRegister* = Register(30) + zeroRegister* = Register(31) + stackPointer* = Register(31) + +proc number(register: Register): uint32 {.inline, raises: [].} = + ## Returns the five-bit encoding of a register. + uint32(register) and 31'u32 + +proc sizeBit(width: Width): uint32 {.inline, raises: [].} = + ## Returns the sf field that selects the 64-bit form. + if width == Word64: 1'u32 shl 31 else: 0'u32 + +proc fail(message: string) {.noreturn, raises: [BasicError].} = + ## Reports a controlled encoding failure. + raise newException(BasicError, "BASIC " & message) + +proc emit(assembler: var Assembler, word: uint32) {.inline, raises: [].} = + ## Appends one encoded instruction. + assembler.code.add(word) + +proc position*(assembler: Assembler): int {.inline, raises: [].} = + ## Returns the index of the next instruction word. + assembler.code.len + +## Labels + +proc label*(assembler: var Assembler): Label {.raises: [].} = + ## Reserves an unplaced branch target. + assembler.targets.add(-1) + Label(assembler.targets.len - 1) + +proc place*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Fixes a label at the current instruction position. + assembler.targets[int(target)] = assembler.code.len + +proc resolve*(assembler: var Assembler) {.raises: [BasicError].} = + ## Patches every recorded branch once all labels are placed. + for fixup in assembler.fixups: + let destination = assembler.targets[fixup.label] + if destination < 0: + fail("assembler label was never placed") + let distance = destination - fixup.at + case fixup.kind + of Branch26Fixup: + if distance < -(1 shl 25) or distance >= (1 shl 25): + fail("assembler branch is out of range") + assembler.code[fixup.at] = assembler.code[fixup.at] or + (uint32(distance) and 0x03FFFFFF'u32) + of Branch19Fixup: + if distance < -(1 shl 18) or distance >= (1 shl 18): + fail("assembler branch is out of range") + assembler.code[fixup.at] = assembler.code[fixup.at] or + ((uint32(distance) and 0x0007FFFF'u32) shl 5) + assembler.fixups.setLen(0) + +## Moves and immediates + +proc moveZero*(assembler: var Assembler, width: Width, destination: Register, + value: uint16, shift = 0) {.raises: [].} = + ## Writes a 16-bit field and zeroes the rest of the register. + let base = if width == Word64: 0xD2800000'u32 else: 0x52800000'u32 + assembler.emit( + base or (uint32(shift div 16) shl 21) or (uint32(value) shl 5) or + destination.number + ) + +proc moveNot*(assembler: var Assembler, width: Width, destination: Register, + value: uint16, shift = 0) {.raises: [].} = + ## Writes the inverse of a 16-bit field into a cleared register. + let base = if width == Word64: 0x92800000'u32 else: 0x12800000'u32 + assembler.emit( + base or (uint32(shift div 16) shl 21) or (uint32(value) shl 5) or + destination.number + ) + +proc moveKeep*(assembler: var Assembler, width: Width, destination: Register, + value: uint16, shift = 0) {.raises: [].} = + ## Overwrites one 16-bit field and keeps the others. + let base = if width == Word64: 0xF2800000'u32 else: 0x72800000'u32 + assembler.emit( + base or (uint32(shift div 16) shl 21) or (uint32(value) shl 5) or + destination.number + ) + +proc logical(assembler: var Assembler, base: uint32, width: Width, + destination, left, right: Register, shift: int) {.inline, raises: [].} = + ## Encodes one shifted-register logical instruction. + assembler.emit( + base or width.sizeBit or (right.number shl 16) or + (uint32(shift) shl 10) or (left.number shl 5) or destination.number + ) + +proc moveRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Copies one register into another through ORR with the zero register. + assembler.logical( + 0x2A000000'u32, width, destination, zeroRegister, source, 0 + ) + +proc loadImmediate*(assembler: var Assembler, width: Width, + destination: Register, value: int64) {.raises: [].} = + ## Materializes any constant using the fewest wide moves. + let bits = + if width == Word64: cast[uint64](value) + else: cast[uint64](value) and 0xFFFFFFFF'u64 + let fields = if width == Word64: 4 else: 2 + var negated = not bits + if width == Word32: + negated = negated and 0xFFFFFFFF'u64 + var zeroCount = 0 + var onesCount = 0 + for index in 0 ..< fields: + let field = uint16((bits shr (index * 16)) and 0xFFFF'u64) + if field == 0: + inc zeroCount + if field == 0xFFFF'u16: + inc onesCount + if onesCount > zeroCount: + var first = true + for index in 0 ..< fields: + let field = uint16((negated shr (index * 16)) and 0xFFFF'u64) + if first: + assembler.moveNot(width, destination, field, index * 16) + first = false + elif field != 0: + let keep = uint16((bits shr (index * 16)) and 0xFFFF'u64) + assembler.moveKeep(width, destination, keep, index * 16) + else: + var first = true + for index in 0 ..< fields: + let field = uint16((bits shr (index * 16)) and 0xFFFF'u64) + if field == 0 and not first: + continue + if first: + assembler.moveZero(width, destination, field, index * 16) + first = false + else: + assembler.moveKeep(width, destination, field, index * 16) + +## Arithmetic + +proc arithmeticImmediate(assembler: var Assembler, base: uint32, width: Width, + destination, source: Register, value: int) {.raises: [BasicError].} = + ## Encodes an add or subtract with a 12-bit unsigned immediate. + if value < 0 or value > 4095: + fail("assembler immediate is out of range") + assembler.emit( + base or width.sizeBit or (uint32(value) shl 10) or + (source.number shl 5) or destination.number + ) + +proc addImmediate*(assembler: var Assembler, width: Width, + destination, source: Register, value: int) {.raises: [BasicError].} = + ## Adds a small unsigned constant. + assembler.arithmeticImmediate( + 0x11000000'u32, width, destination, source, value + ) + +proc subtractImmediate*(assembler: var Assembler, width: Width, + destination, source: Register, value: int) {.raises: [BasicError].} = + ## Subtracts a small unsigned constant. + assembler.arithmeticImmediate( + 0x51000000'u32, width, destination, source, value + ) + +proc compareImmediate*(assembler: var Assembler, width: Width, + source: Register, value: int) {.raises: [BasicError].} = + ## Sets flags from a subtraction, discarding the difference. + assembler.arithmeticImmediate( + 0x71000000'u32, width, zeroRegister, source, value + ) + +proc arithmeticRegister(assembler: var Assembler, base: uint32, width: Width, + destination, left, right: Register, shift: int) {.inline, raises: [].} = + ## Encodes an add or subtract of an optionally shifted register. + assembler.emit( + base or width.sizeBit or (right.number shl 16) or + (uint32(shift) shl 10) or (left.number shl 5) or destination.number + ) + +proc addRegister*(assembler: var Assembler, width: Width, + destination, left, right: Register, shift = 0) {.raises: [].} = + ## Adds two registers, optionally shifting the second left. + assembler.arithmeticRegister( + 0x0B000000'u32, width, destination, left, right, shift + ) + +proc subtractRegister*(assembler: var Assembler, width: Width, + destination, left, right: Register, shift = 0) {.raises: [].} = + ## Subtracts the second register from the first. + assembler.arithmeticRegister( + 0x4B000000'u32, width, destination, left, right, shift + ) + +proc compareRegister*(assembler: var Assembler, width: Width, + left, right: Register) {.raises: [].} = + ## Sets flags from the difference of two registers. + assembler.arithmeticRegister( + 0x6B000000'u32, width, zeroRegister, left, right, 0 + ) + +proc negate*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Writes the two's complement negation of a register. + assembler.arithmeticRegister( + 0x4B000000'u32, width, destination, zeroRegister, source, 0 + ) + +proc multiplyAdd*(assembler: var Assembler, width: Width, + destination, left, right, addend: Register) {.raises: [].} = + ## Computes addend plus the product of two registers. + assembler.emit( + 0x1B000000'u32 or width.sizeBit or (right.number shl 16) or + (addend.number shl 10) or (left.number shl 5) or destination.number + ) + +proc multiplySubtract*(assembler: var Assembler, width: Width, + destination, left, right, minuend: Register) {.raises: [].} = + ## Subtracts the product of two registers from a third. + assembler.emit( + 0x1B008000'u32 or width.sizeBit or (right.number shl 16) or + (minuend.number shl 10) or (left.number shl 5) or destination.number + ) + +proc multiply*(assembler: var Assembler, width: Width, + destination, left, right: Register) {.raises: [].} = + ## Multiplies two registers. + assembler.multiplyAdd(width, destination, left, right, zeroRegister) + +proc signedDivide*(assembler: var Assembler, width: Width, + destination, left, right: Register) {.raises: [].} = + ## Divides with truncation toward zero, yielding zero on a zero divisor. + assembler.emit( + 0x1AC00C00'u32 or width.sizeBit or (right.number shl 16) or + (left.number shl 5) or destination.number + ) + +## Logic + +proc andRegister*(assembler: var Assembler, width: Width, + destination, left, right: Register, shift = 0) {.raises: [].} = + ## Computes a bitwise conjunction. + assembler.logical(0x0A000000'u32, width, destination, left, right, shift) + +proc orRegister*(assembler: var Assembler, width: Width, + destination, left, right: Register, shift = 0) {.raises: [].} = + ## Computes a bitwise disjunction. + assembler.logical(0x2A000000'u32, width, destination, left, right, shift) + +proc xorRegister*(assembler: var Assembler, width: Width, + destination, left, right: Register, shift = 0) {.raises: [].} = + ## Computes a bitwise exclusive disjunction. + assembler.logical(0x4A000000'u32, width, destination, left, right, shift) + +proc notRegister*(assembler: var Assembler, width: Width, + destination, source: Register) {.raises: [].} = + ## Writes the bitwise complement of a register. + assembler.logical( + 0x2A200000'u32, width, destination, zeroRegister, source, 0 + ) + +proc setOnCondition*(assembler: var Assembler, width: Width, + destination: Register, condition: Condition) {.raises: [].} = + ## Writes BASIC's -1 when the condition holds and zero otherwise. + let inverted = uint32(ord(condition)) xor 1'u32 + assembler.emit( + 0x5A800000'u32 or width.sizeBit or (zeroRegister.number shl 16) or + (inverted shl 12) or (zeroRegister.number shl 5) or destination.number + ) + +## Memory + +proc scaledOffset(offset, scale: int): uint32 {.raises: [BasicError].} = + ## Converts a byte offset into the scaled 12-bit immediate field. + if offset < 0 or offset mod scale != 0 or (offset div scale) > 4095: + fail("assembler memory offset is out of range") + uint32(offset div scale) + +proc loadByte*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads one byte, zero-extending into the destination. + assembler.emit( + 0x39400000'u32 or (scaledOffset(offset, 1) shl 10) or + (base.number shl 5) or destination.number + ) + +proc storeByte*(assembler: var Assembler, source, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Stores the low byte of a register. + assembler.emit( + 0x39000000'u32 or (scaledOffset(offset, 1) shl 10) or + (base.number shl 5) or source.number + ) + +proc loadWord*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads 32 bits, zero-extending into the destination. + assembler.emit( + 0xB9400000'u32 or (scaledOffset(offset, 4) shl 10) or + (base.number shl 5) or destination.number + ) + +proc storeWord*(assembler: var Assembler, source, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Stores the low 32 bits of a register. + assembler.emit( + 0xB9000000'u32 or (scaledOffset(offset, 4) shl 10) or + (base.number shl 5) or source.number + ) + +proc loadDouble*(assembler: var Assembler, destination, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Loads 64 bits. + assembler.emit( + 0xF9400000'u32 or (scaledOffset(offset, 8) shl 10) or + (base.number shl 5) or destination.number + ) + +proc storeDouble*(assembler: var Assembler, source, base: Register, + offset = 0) {.raises: [BasicError].} = + ## Stores 64 bits. + assembler.emit( + 0xF9000000'u32 or (scaledOffset(offset, 8) shl 10) or + (base.number shl 5) or source.number + ) + +proc storePair*(assembler: var Assembler, first, second, base: Register, + offset: int, preIndex = false) {.raises: [BasicError].} = + ## Stores two 64-bit registers, optionally updating the base first. + if offset mod 8 != 0 or offset div 8 < -64 or offset div 8 > 63: + fail("assembler memory offset is out of range") + let base32 = if preIndex: 0xA9800000'u32 else: 0xA9000000'u32 + assembler.emit( + base32 or ((uint32(offset div 8) and 0x7F'u32) shl 15) or + (second.number shl 10) or (base.number shl 5) or first.number + ) + +proc loadPair*(assembler: var Assembler, first, second, base: Register, + offset: int, postIndex = false) {.raises: [BasicError].} = + ## Loads two 64-bit registers, optionally updating the base afterward. + if offset mod 8 != 0 or offset div 8 < -64 or offset div 8 > 63: + fail("assembler memory offset is out of range") + let base32 = if postIndex: 0xA8C00000'u32 else: 0xA9400000'u32 + assembler.emit( + base32 or ((uint32(offset div 8) and 0x7F'u32) shl 15) or + (second.number shl 10) or (base.number shl 5) or first.number + ) + +## Branches + +proc branch*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Jumps unconditionally to a label. + assembler.fixups.add( + Fixup(kind: Branch26Fixup, at: assembler.code.len, label: int(target)) + ) + assembler.emit(0x14000000'u32) + +proc branchIf*(assembler: var Assembler, condition: Condition, + target: Label) {.raises: [].} = + ## Jumps to a label when the condition holds. + assembler.fixups.add( + Fixup(kind: Branch19Fixup, at: assembler.code.len, label: int(target)) + ) + assembler.emit(0x54000000'u32 or uint32(ord(condition))) + +proc branchIfZero*(assembler: var Assembler, width: Width, source: Register, + target: Label) {.raises: [].} = + ## Jumps to a label when a register holds zero. + assembler.fixups.add( + Fixup(kind: Branch19Fixup, at: assembler.code.len, label: int(target)) + ) + assembler.emit(0x34000000'u32 or width.sizeBit or source.number) + +proc branchIfNotZero*(assembler: var Assembler, width: Width, + source: Register, target: Label) {.raises: [].} = + ## Jumps to a label when a register holds anything but zero. + assembler.fixups.add( + Fixup(kind: Branch19Fixup, at: assembler.code.len, label: int(target)) + ) + assembler.emit(0x35000000'u32 or width.sizeBit or source.number) + +proc callRegister*(assembler: var Assembler, target: Register) + {.raises: [].} = + ## Calls the address held in a register, setting the link register. + assembler.emit(0xD63F0000'u32 or (target.number shl 5)) + +proc jumpRegister*(assembler: var Assembler, target: Register) + {.raises: [].} = + ## Jumps to the address held in a register without linking. + assembler.emit(0xD61F0000'u32 or (target.number shl 5)) + +proc returnToCaller*(assembler: var Assembler) {.raises: [].} = + ## Returns through the link register. + assembler.emit(0xD65F03C0'u32) + +proc testLowBits*(assembler: var Assembler, width: Width, source: Register, + count: int) {.raises: [BasicError].} = + ## Sets flags from the lowest bits of a register, leaving the result + ## nowhere. The logical immediate for a run of ones starting at bit zero + ## is simply its length minus one. + if count < 1 or count > (if width == Word64: 63 else: 31): + fail("assembler bit count is out of range") + assembler.emit( + 0x72000000'u32 or width.sizeBit or + (if width == Word64: 1'u32 shl 22 else: 0'u32) or + (uint32(count - 1) shl 10) or (source.number shl 5) or 31'u32 + ) + +proc signedMultiplyLong*(assembler: var Assembler, + destination, left, right: Register) {.raises: [].} = + ## Multiplies two 32-bit registers into a full 64-bit result. + assembler.emit( + 0x9B207C00'u32 or (right.number shl 16) or (left.number shl 5) or + destination.number + ) + +proc arithmeticShiftRight*(assembler: var Assembler, width: Width, + destination, source: Register, count: int) {.raises: [BasicError].} = + ## Shifts right, keeping the sign, by a constant. + let last = if width == Word64: 63 else: 31 + if count < 0 or count > last: + fail("assembler shift count is out of range") + let base = + if width == Word64: 0x9340_0000'u32 else: 0x1300_0000'u32 + assembler.emit( + base or (uint32(count) shl 16) or (uint32(last) shl 10) or + (source.number shl 5) or destination.number + ) + +proc offsetOf*(assembler: Assembler, target: Label): int {.raises: [].} = + ## Returns where a label ended up, in instruction words. + assembler.targets[int(target)] + +proc signExtendWord*(assembler: var Assembler, + destination, source: Register) {.raises: [].} = + ## Widens a 32-bit register into a 64-bit one, keeping the sign. + assembler.emit( + 0x93407C00'u32 or (source.number shl 5) or destination.number + ) + +proc shiftLeftImmediate*(assembler: var Assembler, width: Width, + destination, source: Register, count: int) {.raises: [BasicError].} = + ## Shifts left by a constant, which is a bitfield move underneath. + let size = if width == Word64: 64 else: 32 + if count < 0 or count >= size: + fail("assembler shift count is out of range") + let base = if width == Word64: 0xD3400000'u32 else: 0x53000000'u32 + assembler.emit( + base or (uint32((size - count) mod size) shl 16) or + (uint32(size - 1 - count) shl 10) or (source.number shl 5) or + destination.number + ) + +proc shiftRightImmediate*(assembler: var Assembler, width: Width, + destination, source: Register, count: int) {.raises: [BasicError].} = + ## Shifts right without keeping the sign, by a constant. + let size = if width == Word64: 64 else: 32 + if count < 0 or count >= size: + fail("assembler shift count is out of range") + let base = if width == Word64: 0xD3400000'u32 else: 0x53000000'u32 + assembler.emit( + base or (uint32(count) shl 16) or (uint32(size - 1) shl 10) or + (source.number shl 5) or destination.number + ) + +proc branchLink*(assembler: var Assembler, target: Label) {.raises: [].} = + ## Calls a label, leaving the return address in the link register. + assembler.fixups.add( + Fixup(kind: Branch26Fixup, at: assembler.code.len, label: int(target)) + ) + assembler.emit(0x94000000'u32) diff --git a/src/bassy/arm64jit.nim b/src/bassy/arm64jit.nim new file mode 100644 index 0000000..cda3dc0 --- /dev/null +++ b/src/bassy/arm64jit.nim @@ -0,0 +1,1044 @@ +## The AArch64 half of the code generator, included by jit.nim when the +## target is arm64. It supplies the emitters the walker there calls. +## +## x19 context x20 instruction budget x21 work budget +## x22 globals x23 current frame x24 offset table +## x25 array cells x26 arguments x27 frames +## x28 register file +## x9 .. x15 working registers; x16 far addresses; x17 one cell +## +## Everything long lived sits in a register the platform's convention +## keeps across a call, so calling back into the interpreter's code +## costs no saving beyond the two budgets it may charge. + +const + Context = x19 + Instructions = x20 + Work = x21 + GlobalsBase = x22 + RegistersBase = x23 + TableBase = x24 + MemoryBase = x25 + ArgumentsBase = x26 + FramesBase = x27 + FileBase = x28 + Temps = [x9, x10, x11, x12, x13, x14, x15] + Far = x16 + Cell = x17 + FrameBytes = 96 + NearBytes = 4095 - ValuePayload + +proc temp(index: int): Register {.inline, raises: [].} = + ## Returns one working register. + Temps[index] + +proc nativeCondition(check: Check): Condition {.raises: [].} = + ## Maps a neutral outcome onto the architecture's encoding. + case check + of EqualCheck: EqualCondition + of NotEqualCheck: NotEqualCondition + of LessCheck: LessCondition + of LessEqualCheck: LessEqualCondition + of GreaterCheck: GreaterCondition + of GreaterEqualCheck: GreaterEqualCondition + +proc inverse(condition: Condition): Condition {.raises: [].} = + ## Returns the condition that holds exactly when this one does not. + Condition(ord(condition) xor 1) + +type + Emitter = object + ## The assembler plus whether branches must reach anywhere at all. + code: Assembler + far: bool + limit: int + outside: Label + +proc label(e: var Emitter): Label {.inline, raises: [].} = + ## Reserves a label. + e.code.label() + +proc place(e: var Emitter, target: Label) {.inline, raises: [].} = + ## Places a label here. + e.code.place(target) + +proc jump(e: var Emitter, target: Label) {.raises: [].} = + ## Jumps unconditionally. + e.code.branch(target) + +proc jumpWhen(e: var Emitter, condition: Condition, target: Label) + {.raises: [].} = + ## Jumps when a condition holds, however far away the target is. + if e.far: + let skip = e.code.label() + e.code.branchIf(condition.inverse, skip) + e.code.branch(target) + e.code.place(skip) + else: + e.code.branchIf(condition, target) + +proc jumpIfZero(e: var Emitter, register: Register, target: Label) + {.raises: [].} = + ## Jumps when a working register holds zero. + if e.far: + let skip = e.code.label() + e.code.branchIfNotZero(Word32, register, skip) + e.code.branch(target) + e.code.place(skip) + else: + e.code.branchIfZero(Word32, register, target) + +proc jumpIfNotZero(e: var Emitter, register: Register, target: Label) + {.raises: [].} = + ## Jumps when a working register holds anything but zero. + if e.far: + let skip = e.code.label() + e.code.branchIfZero(Word32, register, skip) + e.code.branch(target) + e.code.place(skip) + else: + e.code.branchIfNotZero(Word32, register, target) + +proc withinProgram(e: var Emitter, offset: Register) + {.raises: [BasicError].} = + ## Sends an offset that is not one of the program's own to the block + ## past the end, which refuses it, instead of reading a table entry + ## that is not there. One unsigned comparison covers both ends. + e.code.loadImmediate(Word32, Temps[6], int64(e.limit)) + e.code.compareRegister(Word32, offset, Temps[6]) + e.jumpWhen(CarrySetCondition, e.outside) + +proc reach(e: var Emitter, place: Place): (Register, int) + {.raises: [BasicError].} = + ## Returns a base register and byte offset for a value, working the + ## address out in full when the offset is too wide to encode. + var base = Cell + case place.home + of SlotHome: base = RegistersBase + of GlobalHome: base = GlobalsBase + of ArgumentHome: base = ArgumentsBase + of HostHome: + e.code.loadDouble(Cell, Context, ContextHostData) + of CellHome: + return (Cell, 0) + let offset = int(place.index) * ValueStride + if offset <= NearBytes: + return (base, offset) + e.code.loadImmediate(Word64, Far, int64(offset)) + e.code.addRegister(Word64, Far, base, Far) + (Far, 0) + +proc readValue(e: var Emitter, value, tag: int, place: Place) + {.raises: [BasicError].} = + ## Reads a value's kind and its 32-bit payload. + let (base, offset) = e.reach(place) + e.code.loadByte(temp(tag), base, offset) + e.code.loadWord(temp(value), base, offset + ValuePayload) + +proc writeWhole(e: var Emitter, place: Place, value: int) + {.raises: [BasicError].} = + ## Writes a whole number. + let (base, offset) = e.reach(place) + e.code.storeByte(zeroRegister, base, offset) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeKind(e: var Emitter, place: Place, tag, value: int) + {.raises: [BasicError].} = + ## Writes a payload under the kind held in a working register. + let (base, offset) = e.reach(place) + e.code.storeByte(temp(tag), base, offset) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeFixed(e: var Emitter, place: Place, value: int) + {.raises: [BasicError].} = + ## Writes a fixed-point payload. + let (base, offset) = e.reach(place) + e.code.loadImmediate(Word32, temp(5), FixedTag) + e.code.storeByte(temp(5), base, offset) + e.code.storeWord(temp(value), base, offset + ValuePayload) + +proc writeConstant(e: var Emitter, place: Place, tag: int, bits: int32) + {.raises: [BasicError].} = + ## Writes a constant of a known kind. + e.code.loadImmediate(Word32, temp(6), int64(bits)) + let (base, offset) = e.reach(place) + if tag == 0: + e.code.storeByte(zeroRegister, base, offset) + else: + e.code.loadImmediate(Word32, temp(5), int64(tag)) + e.code.storeByte(temp(5), base, offset) + e.code.storeWord(temp(6), base, offset + ValuePayload) + +proc copyValue(e: var Emitter, destination, source: Place) + {.raises: [BasicError].} = + ## Copies a value entire, whatever kind it holds, as the interpreter + ## does. + let (fromBase, fromOffset) = e.reach(source) + e.code.loadDouble(temp(5), fromBase, fromOffset) + e.code.loadDouble(temp(6), fromBase, fromOffset + ValuePayload) + let (toBase, toOffset) = e.reach(destination) + e.code.storeDouble(temp(5), toBase, toOffset) + e.code.storeDouble(temp(6), toBase, toOffset + ValuePayload) + +proc unlessWhole(e: var Emitter, tag: int, slow: Label) + {.raises: [].} = + ## Takes the slow path unless a kind says whole number. + e.jumpIfNotZero(temp(tag), slow) + +proc unlessNumeric(e: var Emitter, tag: int, slow: Label) + {.raises: [BasicError].} = + ## Takes the slow path unless a kind says number of either sort. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.jumpWhen(UnsignedGreaterCondition, slow) + +proc unlessSame(e: var Emitter, tag, other: int, slow: Label) + {.raises: [].} = + ## Takes the slow path unless two kinds agree. + e.code.compareRegister(Word32, temp(tag), temp(other)) + e.jumpWhen(NotEqualCondition, slow) + +proc jumpIfSame(e: var Emitter, tag, other: int, target: Label) + {.raises: [].} = + ## Jumps when two kinds agree. + e.code.compareRegister(Word32, temp(tag), temp(other)) + e.jumpWhen(EqualCondition, target) + +proc whenWhole(e: var Emitter, tag: int, target: Label) {.raises: [].} = + ## Jumps when a kind says whole number. + e.jumpIfZero(temp(tag), target) + +proc unlessFixed(e: var Emitter, tag: int, slow: Label) + {.raises: [BasicError].} = + ## Takes the slow path unless a kind says fixed point. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.jumpWhen(NotEqualCondition, slow) + +proc toFixed(e: var Emitter, value: int, slow: Label) + {.raises: [BasicError].} = + ## Turns a whole number into Q16.16 bits. One outside the fixed-point + ## range cannot be, which the interpreter refuses, so that goes slow. + let register = temp(value) + e.code.loadImmediate(Word32, temp(6), 32767) + e.code.compareRegister(Word32, register, temp(6)) + e.jumpWhen(GreaterCondition, slow) + e.code.loadImmediate(Word32, temp(6), -32768) + e.code.compareRegister(Word32, register, temp(6)) + e.jumpWhen(LessCondition, slow) + e.code.shiftLeftImmediate(Word32, register, register, FixedShift) + +proc scaleWide(e: var Emitter, value, tag: int) {.raises: [BasicError].} = + ## Widens a number of either kind to sixty-four bits on the fixed-point + ## scale, where every whole number and every fixed-point one compare + ## exactly, as the interpreter compares them. + let register = temp(value) + let done = e.label() + e.code.signExtendWord(register, register) + e.jumpIfNotZero(temp(tag), done) + e.code.shiftLeftImmediate(Word64, register, register, FixedShift) + e.place(done) + +proc loadWide(e: var Emitter, value: int, bits: int64) {.raises: [].} = + ## Loads a sixty-four bit constant into a working register. + e.code.loadImmediate(Word64, temp(value), bits) + +proc compareWide(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two widened working registers. + e.code.compareRegister(Word64, temp(left), temp(right)) + +proc whenFixed(e: var Emitter, tag: int, target: Label) + {.raises: [BasicError].} = + ## Jumps when a kind says fixed point. + e.code.compareImmediate(Word32, temp(tag), FixedTag) + e.code.branchIf(EqualCondition, target) + +proc loadConstant(e: var Emitter, value: int, bits: int32) + {.raises: [].} = + ## Loads a constant into a working register. + e.code.loadImmediate(Word32, temp(value), int64(bits)) + +proc add(e: var Emitter, left, right: int) {.raises: [].} = + ## Adds, wrapping. + e.code.addRegister(Word32, temp(left), temp(left), temp(right)) + +proc subtract(e: var Emitter, left, right: int) {.raises: [].} = + ## Subtracts, wrapping. + e.code.subtractRegister(Word32, temp(left), temp(left), temp(right)) + +proc multiply(e: var Emitter, left, right: int) {.raises: [].} = + ## Multiplies, wrapping. + e.code.multiply(Word32, temp(left), temp(left), temp(right)) + +proc negate(e: var Emitter, value: int) {.raises: [].} = + ## Negates, wrapping. + e.code.negate(Word32, temp(value), temp(value)) + +proc bitAnd(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits both hold. + e.code.andRegister(Word32, temp(left), temp(left), temp(right)) + +proc bitOr(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits either holds. + e.code.orRegister(Word32, temp(left), temp(left), temp(right)) + +proc bitXor(e: var Emitter, left, right: int) {.raises: [].} = + ## Keeps the bits exactly one holds. + e.code.xorRegister(Word32, temp(left), temp(left), temp(right)) + +proc bitNot(e: var Emitter, value: int) {.raises: [].} = + ## Flips every bit. + e.code.notRegister(Word32, temp(value), temp(value)) + +proc quotient(e: var Emitter, left, right: int) {.raises: [].} = + ## Divides toward zero; the divisor is known not to be zero. The most + ## negative number over minus one wraps back to itself here, which is + ## the answer the interpreter defines. + e.code.signedDivide(Word32, temp(left), temp(left), temp(right)) + +proc remainder(e: var Emitter, left, right: int) {.raises: [].} = + ## Leaves what dividing left over, with the sign of the dividend. + e.code.signedDivide(Word32, temp(6), temp(left), temp(right)) + e.code.multiplySubtract(Word32, temp(left), temp(6), temp(right), + temp(left)) + +proc multiplyFixed(e: var Emitter, left, right: int) + {.raises: [BasicError].} = + ## Multiplies two Q16.16 numbers through a widened intermediate, + ## rounding to nearest exactly as the fixed-point library does. + e.code.signedMultiplyLong(temp(left), temp(left), temp(right)) + e.code.loadImmediate(Word64, temp(6), FixedRounding) + e.code.addRegister(Word64, temp(left), temp(left), temp(6)) + e.code.arithmeticShiftRight(Word64, temp(left), temp(left), FixedShift) + e.code.moveRegister(Word32, temp(left), temp(left)) + +proc widenToFixed(e: var Emitter, value, tag: int, slow: Label) + {.raises: [BasicError].} = + ## Turns a number of either kind into its Q16.16 bits, widened to + ## sixty-four. A whole number outside the fixed-point range cannot + ## become one, which the interpreter refuses, so that goes slow. + let register = temp(value) + let already = e.label() + let ready = e.label() + e.whenFixed(tag, already) + e.code.loadImmediate(Word32, temp(6), 32767) + e.code.compareRegister(Word32, register, temp(6)) + e.jumpWhen(GreaterCondition, slow) + e.code.loadImmediate(Word32, temp(6), -32768) + e.code.compareRegister(Word32, register, temp(6)) + e.jumpWhen(LessCondition, slow) + e.code.signExtendWord(register, register) + e.code.shiftLeftImmediate(Word64, register, register, FixedShift) + e.jump(ready) + e.place(already) + e.code.signExtendWord(register, register) + e.place(ready) + +proc divideFixed(e: var Emitter, left, right: int, slow: Label) + {.raises: [BasicError].} = + ## Divides two widened Q16.16 numbers, rounding to nearest with halves + ## going up, for either sign, exactly as the fixed-point library + ## does: the signs are put right first, half the divisor is added, + ## and the truncating divide is corrected back to a floor. + let numerator = temp(left) + let denominator = temp(right) + let answer = temp(5) + let leftOver = temp(6) + e.code.compareImmediate(Word64, denominator, 0) + e.jumpWhen(EqualCondition, slow) + let signsSettled = e.label() + e.code.branchIf(GreaterCondition, signsSettled) + e.code.negate(Word64, numerator, numerator) + e.code.negate(Word64, denominator, denominator) + e.place(signsSettled) + e.code.shiftLeftImmediate(Word64, numerator, numerator, FixedShift) + e.code.shiftRightImmediate(Word64, answer, denominator, 1) + e.code.addRegister(Word64, numerator, numerator, answer) + e.code.signedDivide(Word64, answer, numerator, denominator) + e.code.multiplySubtract(Word64, leftOver, answer, denominator, + numerator) + let done = e.label() + e.code.compareImmediate(Word64, leftOver, 0) + e.code.branchIf(EqualCondition, done) + e.code.compareImmediate(Word64, numerator, 0) + e.code.branchIf(GreaterEqualCondition, done) + e.code.subtractImmediate(Word64, answer, answer, 1) + e.place(done) + e.code.moveRegister(Word32, numerator, answer) + +proc compare(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two working registers. + e.code.compareRegister(Word32, temp(left), temp(right)) + +proc compareConstant(e: var Emitter, value: int, bits: int32) + {.raises: [BasicError].} = + ## Sets flags from a working register against a constant. + if bits >= 0 and bits <= 4095: + e.code.compareImmediate(Word32, temp(value), int(bits)) + else: + e.code.loadImmediate(Word32, temp(6), int64(bits)) + e.code.compareRegister(Word32, temp(value), temp(6)) + +proc answer(e: var Emitter, value: int, check: Check) {.raises: [].} = + ## Writes BASIC's -1 for true and zero for false. + e.code.setOnCondition(Word32, temp(value), nativeCondition(check)) + +proc jumpOn(e: var Emitter, check: Check, target: Label) + {.raises: [].} = + ## Jumps on a comparison outcome. + e.jumpWhen(nativeCondition(check), target) + +proc jumpIfZeroValue(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a working register holds zero. + e.jumpIfZero(temp(value), target) + +proc jumpIfNotZeroValue(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a working register holds anything but zero. + e.jumpIfNotZero(temp(value), target) + +proc jumpIfLowBits(e: var Emitter, value, bits: int, target: Label) + {.raises: [BasicError].} = + ## Jumps when any of a working register's lowest bits is set. + e.code.testLowBits(Word32, temp(value), bits) + e.jumpWhen(NotEqualCondition, target) + +proc cellAddress(e: var Emitter, index: int, extent: ArrayExtent, + slow: Label) {.raises: [].} = + ## Bounds checks an index and leaves the cell's address in Cell. One + ## unsigned comparison covers both ends, as the interpreter's does. + let position = temp(index) + e.code.loadImmediate(Word32, temp(6), int64(extent.length)) + e.code.compareRegister(Word32, position, temp(6)) + e.jumpWhen(CarrySetCondition, slow) + e.code.loadImmediate(Word32, temp(6), int64(extent.base)) + e.code.addRegister(Word32, temp(6), temp(6), position) + e.code.addRegister(Word64, Cell, MemoryBase, temp(6), 4) + +proc charge(e: var Emitter, instructions, work: int32) + {.raises: [BasicError].} = + ## Charges both budgets, already known to cover it, without looking. + if instructions <= 4095: + e.code.subtractImmediate(Word64, Instructions, Instructions, + int(instructions)) + else: + e.code.loadImmediate(Word64, temp(5), int64(instructions)) + e.code.subtractRegister(Word64, Instructions, Instructions, temp(5)) + if work <= 4095: + e.code.subtractImmediate(Word64, Work, Work, int(work)) + else: + e.code.loadImmediate(Word64, temp(6), int64(work)) + e.code.subtractRegister(Word64, Work, Work, temp(6)) + +proc meter(e: var Emitter, instructions, work: int32, slow: Label, + needInstructions = int64(instructions), needWork = int64(work)) + {.raises: [BasicError].} = + ## Checks both budgets hold what is needed before charging either, as + ## the interpreter does. What is needed can be more than this block + ## costs, when one look is to cover every block until the next. + for (budget, need) in [(Instructions, needInstructions), + (Work, needWork)]: + if need <= 4095: + e.code.compareImmediate(Word64, budget, int(need)) + else: + e.code.loadImmediate(Word64, temp(5), need) + e.code.compareRegister(Word64, budget, temp(5)) + e.jumpWhen(LessCondition, slow) + e.charge(instructions, work) + +proc frameOf(e: var Emitter, base: Register, index: Register) + {.raises: [].} = + ## Points a register at one register-file slot by its absolute index. + e.code.addRegister(Word64, base, FileBase, index, 4) + +proc copyValues(e: var Emitter, destination, source: Register, + count: int) {.raises: [BasicError].} = + ## Copies a run of whole values, in a loop once there are many. + if count <= 8: + for index in 0 ..< count: + e.code.loadDouble(temp(5), source, index * ValueStride) + e.code.loadDouble(temp(6), source, index * ValueStride + ValuePayload) + e.code.storeDouble(temp(5), destination, index * ValueStride) + e.code.storeDouble(temp(6), destination, + index * ValueStride + ValuePayload) + return + e.code.moveRegister(Word64, temp(2), source) + e.code.moveRegister(Word64, temp(3), destination) + e.code.loadImmediate(Word32, temp(4), int64(count)) + let again = e.label() + e.place(again) + e.code.loadDouble(temp(5), temp(2), 0) + e.code.loadDouble(temp(6), temp(2), ValuePayload) + e.code.storeDouble(temp(5), temp(3), 0) + e.code.storeDouble(temp(6), temp(3), ValuePayload) + e.code.addImmediate(Word64, temp(2), temp(2), ValueStride) + e.code.addImmediate(Word64, temp(3), temp(3), ValueStride) + e.code.subtractImmediate(Word32, temp(4), temp(4), 1) + e.code.branchIfNotZero(Word32, temp(4), again) + +proc clearValues(e: var Emitter, destination: Register, count: int) + {.raises: [BasicError].} = + ## Zeroes a run of values, in a loop once there are many. + if count <= 8: + for index in 0 ..< count: + e.code.storeDouble(zeroRegister, destination, index * ValueStride) + e.code.storeDouble(zeroRegister, destination, + index * ValueStride + ValuePayload) + return + e.code.moveRegister(Word64, temp(3), destination) + e.code.loadImmediate(Word32, temp(4), int64(count)) + let again = e.label() + e.place(again) + e.code.storeDouble(zeroRegister, temp(3), 0) + e.code.storeDouble(zeroRegister, temp(3), ValuePayload) + e.code.addImmediate(Word64, temp(3), temp(3), ValueStride) + e.code.subtractImmediate(Word32, temp(4), temp(4), 1) + e.code.branchIfNotZero(Word32, temp(4), again) + +proc enterRoutine(e: var Emitter, gosub: bool, calleeId: int32, + calleeRegisters, calleeParameters, callerRegisters: int32, + resumeAt: int32, limits: CallLimits, slow: Label) + {.raises: [BasicError].} = + ## Pushes a frame into the interpreter's own array and moves the + ## current frame on, refusing the same two ceilings it refuses. + let depth = temp(0) + let oldBase = temp(1) + let newBase = temp(2) + let frame = temp(3) + e.code.loadWord(depth, Context, ContextDepth) + e.code.loadImmediate(Word32, temp(6), int64(limits.frames) - 1) + e.code.compareRegister(Word32, depth, temp(6)) + e.jumpWhen(GreaterEqualCondition, slow) + e.code.loadWord(oldBase, Context, ContextBase) + e.code.loadImmediate(Word32, temp(6), int64(callerRegisters)) + e.code.addRegister(Word32, newBase, oldBase, temp(6)) + e.code.loadImmediate(Word32, temp(6), + int64(limits.slots) - int64(calleeRegisters)) + e.code.compareRegister(Word32, newBase, temp(6)) + e.jumpWhen(GreaterCondition, slow) + + e.code.addRegister(Word64, frame, FramesBase, depth, 4) + e.code.storeWord(oldBase, frame, FrameBase) + e.code.loadWord(temp(4), Context, ContextRoutine) + e.code.storeWord(temp(4), frame, FrameRoutine) + e.code.loadImmediate(Word32, temp(4), int64(resumeAt)) + e.code.storeWord(temp(4), frame, FrameReturn) + if gosub: + e.code.loadImmediate(Word32, temp(4), 1) + e.code.storeWord(temp(4), frame, FrameTag) + else: + e.code.storeWord(zeroRegister, frame, FrameTag) + + e.code.addImmediate(Word32, depth, depth, 1) + e.code.storeWord(depth, Context, ContextDepth) + e.code.storeWord(newBase, Context, ContextBase) + e.code.loadImmediate(Word32, temp(4), int64(calleeId)) + e.code.storeWord(temp(4), Context, ContextRoutine) + + # A GOSUB hands the callee a copy of the caller's slots; a call clears + # them and lays the arguments over the first few, in that order. + e.code.moveRegister(Word64, frame, RegistersBase) + e.frameOf(RegistersBase, newBase) + if gosub: + e.copyValues(RegistersBase, frame, int(calleeRegisters)) + else: + e.clearValues(RegistersBase, int(calleeRegisters)) + e.copyValues(RegistersBase, ArgumentsBase, int(calleeParameters)) + +proc leaveRoutine(e: var Emitter, parameters: int32, exitSub: bool, + slow: Label) {.raises: [BasicError].} = + ## Pops a frame and jumps to wherever it said to carry on. A GOSUB + ## frame first hands the shared parameters back to the caller. Leaving + ## a sub outright only goes this way when its own frame is on top. + ## Nothing is written until every refusal has been passed, the offset + ## it would carry on at included, so a refusal leaves the frame on. + let depth = temp(0) + let frame = temp(1) + let base = temp(2) + let resume = temp(3) + e.code.loadWord(depth, Context, ContextDepth) + e.jumpIfZero(depth, slow) + e.code.subtractImmediate(Word32, depth, depth, 1) + e.code.addRegister(Word64, frame, FramesBase, depth, 4) + if exitSub: + e.code.loadByte(temp(4), frame, FrameTag) + e.jumpIfNotZero(temp(4), slow) + e.code.loadWord(resume, frame, FrameReturn) + e.withinProgram(resume) + e.code.storeWord(depth, Context, ContextDepth) + if parameters > 0: + let plain = e.label() + e.code.loadByte(temp(4), frame, FrameTag) + e.code.compareImmediate(Word32, temp(4), 1) + e.code.branchIf(NotEqualCondition, plain) + e.code.loadWord(base, frame, FrameBase) + e.frameOf(Far, base) + e.copyValues(Far, RegistersBase, int(parameters)) + e.place(plain) + # A long copy works through the same registers, so what it needs from + # the frame is read again after it rather than kept across it. + e.code.loadWord(base, frame, FrameBase) + e.code.loadWord(resume, frame, FrameReturn) + e.code.storeWord(base, Context, ContextBase) + e.code.loadWord(temp(4), frame, FrameRoutine) + e.code.storeWord(temp(4), Context, ContextRoutine) + e.code.storeWord(resume, Context, ContextOffset) + e.frameOf(RegistersBase, base) + e.code.addRegister(Word64, temp(4), TableBase, resume, 3) + e.code.loadDouble(temp(4), temp(4), 0) + e.code.jumpRegister(temp(4)) + +proc callSlow(e: var Emitter, offset: int32, routine: Label) + {.raises: [].} = + ## Runs the interpreter's own code for one instruction. + e.code.loadImmediate(Word32, x1, int64(offset)) + e.code.branchLink(routine) + +proc slowRoutine(e: var Emitter, failed: Label, helper: int) + {.raises: [BasicError].} = + ## The one place compiled code calls out. The budgets go into the + ## context for the interpreter's code to charge, and come back from it + ## along with the frame, since a call or a return may have moved it. + ## Every storage base comes back too: host code may have replaced a + ## buffer, and nothing held from before the call may be trusted after. + e.code.storePair(framePointer, linkRegister, stackPointer, -16, true) + e.code.storeDouble(Instructions, Context, ContextInstructions) + e.code.storeDouble(Work, Context, ContextWork) + e.code.moveRegister(Word64, x0, Context) + e.code.loadDouble(temp(0), Context, helper) + e.code.callRegister(temp(0)) + e.code.moveRegister(Word32, temp(0), x0) + e.code.loadDouble(Instructions, Context, ContextInstructions) + e.code.loadDouble(Work, Context, ContextWork) + e.code.loadDouble(GlobalsBase, Context, 0) + e.code.loadDouble(MemoryBase, Context, ContextMemory) + e.code.loadDouble(ArgumentsBase, Context, ContextArguments) + e.code.loadDouble(FramesBase, Context, ContextFrames) + e.code.loadDouble(FileBase, Context, ContextRegisterFile) + e.code.loadWord(temp(1), Context, ContextBase) + e.frameOf(RegistersBase, temp(1)) + e.code.loadPair(framePointer, linkRegister, stackPointer, 16, true) + e.code.branchIfNotZero(Word32, temp(0), failed) + e.code.returnToCaller() + +proc dispatch(e: var Emitter) {.raises: [BasicError].} = + ## Jumps to the block for whatever offset the context names. + e.code.loadWord(temp(0), Context, ContextOffset) + e.withinProgram(temp(0)) + e.code.addRegister(Word64, temp(1), TableBase, temp(0), 3) + e.code.loadDouble(temp(1), temp(1), 0) + e.code.jumpRegister(temp(1)) + +proc prologue(e: var Emitter) {.raises: [BasicError].} = + ## Saves what the platform says to keep and loads the machine state. + e.code.storePair(framePointer, linkRegister, stackPointer, -FrameBytes, + true) + e.code.storePair(x19, x20, stackPointer, 16) + e.code.storePair(x21, x22, stackPointer, 32) + e.code.storePair(x23, x24, stackPointer, 48) + e.code.storePair(x25, x26, stackPointer, 64) + e.code.storePair(x27, x28, stackPointer, 80) + e.code.moveRegister(Word64, Context, x0) + e.code.loadDouble(GlobalsBase, Context, 0) + e.code.loadDouble(Instructions, Context, ContextInstructions) + e.code.loadDouble(Work, Context, ContextWork) + e.code.loadDouble(FileBase, Context, ContextRegisterFile) + e.code.loadDouble(TableBase, Context, ContextTable) + e.code.loadDouble(MemoryBase, Context, ContextMemory) + e.code.loadDouble(ArgumentsBase, Context, ContextArguments) + e.code.loadDouble(FramesBase, Context, ContextFrames) + e.code.loadWord(temp(0), Context, ContextBase) + e.frameOf(RegistersBase, temp(0)) + +proc restoreAndReturn(e: var Emitter) {.raises: [BasicError].} = + ## Restores what the platform says to keep and returns x0 as it is. + e.code.loadPair(x19, x20, stackPointer, 16) + e.code.loadPair(x21, x22, stackPointer, 32) + e.code.loadPair(x23, x24, stackPointer, 48) + e.code.loadPair(x25, x26, stackPointer, 64) + e.code.loadPair(x27, x28, stackPointer, 80) + e.code.loadPair(framePointer, linkRegister, stackPointer, FrameBytes, + true) + e.code.returnToCaller() + +proc epilogue(e: var Emitter, status: NativeStatus) + {.raises: [BasicError].} = + ## Restores what the platform says to keep and returns a status. + e.code.loadImmediate(Word32, x0, int64(ord(status))) + e.restoreAndReturn() + +proc leaveWithAnswer(e: var Emitter) {.raises: [BasicError].} = + ## Returns whatever status the interpreter's code answered with. + e.code.moveRegister(Word32, x0, temp(0)) + e.restoreAndReturn() + +## Globals held in registers +## +## Inside a specialised loop its globals live in the registers below, +## proved whole numbers on the way in. Nothing in such a loop calls out, +## so registers the convention lets a callee clobber are safe to use. + +const Hoisting* = [x0, x1, x2, x3, x4, x5, x6, x7] + +proc hoisted(slot: int): Register {.inline, raises: [].} = + ## Returns the register holding one hoisted global. + Hoisting[slot] + +proc globalAt(e: var Emitter, index: int32): (Register, int) + {.raises: [BasicError].} = + ## Returns a base register and byte offset for one global. + e.reach(global(index)) + +proc guardHoisted(e: var Emitter, index: int32, failed: Label) + {.raises: [BasicError].} = + ## Leaves for the general code unless a global holds a whole number. + let (base, offset) = e.globalAt(index) + e.code.loadByte(temp(0), base, offset) + e.jumpIfNotZero(temp(0), failed) + +proc loadHoisted(e: var Emitter, slot: int, index: int32) + {.raises: [BasicError].} = + ## Reads one global's payload into its register. + let (base, offset) = e.globalAt(index) + e.code.loadWord(hoisted(slot), base, offset + ValuePayload) + +proc storeHoisted(e: var Emitter, slot: int, index: int32) + {.raises: [BasicError].} = + ## Publishes one register back as a whole number. + let (base, offset) = e.globalAt(index) + e.code.storeByte(zeroRegister, base, offset) + e.code.storeWord(hoisted(slot), base, offset + ValuePayload) + +proc beginHoisting(e: var Emitter) {.raises: [].} = + ## The globals stay addressable throughout, so nothing to prepare. + discard + +proc endHoisting(e: var Emitter) {.raises: [].} = + ## Nothing borrowed, so nothing to give back. + discard + +proc setHoisted(e: var Emitter, slot: int, bits: int32) {.raises: [].} = + ## Loads a constant into a hoisted global. + e.code.loadImmediate(Word32, hoisted(slot), int64(bits)) + +proc copyHoisted(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Copies one hoisted global into another. + e.code.moveRegister(Word32, hoisted(destination), hoisted(source)) + +proc addHoisted(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Adds one hoisted global into another, wrapping. + e.code.addRegister(Word32, hoisted(destination), hoisted(destination), + hoisted(source)) + +proc addHoistedConstant(e: var Emitter, slot: int, bits: int32) + {.raises: [BasicError].} = + ## Adds a constant to a hoisted global, wrapping. + let target = hoisted(slot) + if bits >= 0 and bits <= 4095: + e.code.addImmediate(Word32, target, target, int(bits)) + elif bits < 0 and bits >= -4095: + e.code.subtractImmediate(Word32, target, target, int(-bits)) + else: + e.code.loadImmediate(Word32, temp(6), int64(bits)) + e.code.addRegister(Word32, target, target, temp(6)) + +proc hoistedToTemp(e: var Emitter, value, slot: int) {.raises: [].} = + ## Copies a hoisted global into a working register. + e.code.moveRegister(Word32, temp(value), hoisted(slot)) + +proc tempToHoisted(e: var Emitter, slot, value: int) {.raises: [].} = + ## Copies a working register into a hoisted global. + e.code.moveRegister(Word32, hoisted(slot), temp(value)) + +proc addTempToHoisted(e: var Emitter, slot, value: int) {.raises: [].} = + ## Adds a working register into a hoisted global, wrapping. + e.code.addRegister(Word32, hoisted(slot), hoisted(slot), temp(value)) + +proc compareHoisted(e: var Emitter, slot: int, bits: int32) + {.raises: [BasicError].} = + ## Sets flags from a hoisted global against a constant. + if bits >= 0 and bits <= 4095: + e.code.compareImmediate(Word32, hoisted(slot), int(bits)) + else: + e.code.loadImmediate(Word32, temp(6), int64(bits)) + e.code.compareRegister(Word32, hoisted(slot), temp(6)) + +## Values held in registers across a block +## +## A block's fast version keeps the slots and globals it touches in the +## registers below. Nothing in the pool outlives the block: every held +## value is written back before anything else could look at memory. + +const + Pool* = [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13] + FastScratch = x14 + +proc pooled(index: int): Register {.inline, raises: [].} = + ## Returns one pool register. + Pool[index] + +proc fastLoad(e: var Emitter, payload, tag: int, place: Place, + deopt: Label) {.raises: [BasicError].} = + ## Reads a value's kind and payload, taking the deopt path unless it + ## is a number of either kind. + let (base, offset) = e.reach(place) + e.code.loadByte(pooled(tag), base, offset) + e.code.loadWord(pooled(payload), base, offset + ValuePayload) + e.code.compareImmediate(Word32, pooled(tag), FixedTag) + e.jumpWhen(UnsignedGreaterCondition, deopt) + +proc fastStore(e: var Emitter, place: Place, payload, tag, kind: int) + {.raises: [BasicError].} = + ## Writes a value back: its kind from a register, or the known kind. + let (base, offset) = e.reach(place) + if tag >= 0: + e.code.storeByte(pooled(tag), base, offset) + elif kind == 0: + e.code.storeByte(zeroRegister, base, offset) + else: + e.code.loadImmediate(Word32, FastScratch, int64(kind)) + e.code.storeByte(FastScratch, base, offset) + e.code.storeWord(pooled(payload), base, offset + ValuePayload) + +proc fastMove(e: var Emitter, destination, source: int) {.raises: [].} = + ## Copies one pool register into another. + e.code.moveRegister(Word32, pooled(destination), pooled(source)) + +proc fastConstant(e: var Emitter, destination: int, bits: int32) + {.raises: [].} = + ## Loads a constant into a pool register. + e.code.loadImmediate(Word32, pooled(destination), int64(bits)) + +proc fastAdd(e: var Emitter, destination, source: int) {.raises: [].} = + ## Adds, wrapping. + e.code.addRegister(Word32, pooled(destination), pooled(destination), + pooled(source)) + +proc fastSubtract(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Subtracts, wrapping. + e.code.subtractRegister(Word32, pooled(destination), + pooled(destination), pooled(source)) + +proc fastMultiply(e: var Emitter, destination, source: int) + {.raises: [].} = + ## Multiplies, wrapping. + e.code.multiply(Word32, pooled(destination), pooled(destination), + pooled(source)) + +proc fastMultiplyFixed(e: var Emitter, destination, source: int) + {.raises: [BasicError].} = + ## Multiplies two Q16.16 numbers, rounding as the library does. + let target = pooled(destination) + e.code.signedMultiplyLong(target, target, pooled(source)) + e.code.loadImmediate(Word64, FastScratch, FixedRounding) + e.code.addRegister(Word64, target, target, FastScratch) + e.code.arithmeticShiftRight(Word64, target, target, FixedShift) + e.code.moveRegister(Word32, target, target) + +proc fastNegate(e: var Emitter, destination: int) {.raises: [].} = + ## Negates, wrapping. + e.code.negate(Word32, pooled(destination), pooled(destination)) + +proc fastToFixed(e: var Emitter, destination: int, deopt: Label) + {.raises: [BasicError].} = + ## Turns a whole number into Q16.16 bits, or takes the deopt path when + ## it is outside the fixed-point range, which the interpreter refuses. + let target = pooled(destination) + e.code.loadImmediate(Word32, FastScratch, 32767) + e.code.compareRegister(Word32, target, FastScratch) + e.jumpWhen(GreaterCondition, deopt) + e.code.loadImmediate(Word32, FastScratch, -32768) + e.code.compareRegister(Word32, target, FastScratch) + e.jumpWhen(LessCondition, deopt) + e.code.shiftLeftImmediate(Word32, target, target, FixedShift) + +proc fastCompare(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two pool registers. + e.code.compareRegister(Word32, pooled(left), pooled(right)) + +proc fastCompareWide(e: var Emitter, left, right: int) {.raises: [].} = + ## Sets flags from two widened pool registers. + e.code.compareRegister(Word64, pooled(left), pooled(right)) + +proc fastCompareConstant(e: var Emitter, value: int, bits: int32) + {.raises: [BasicError].} = + ## Sets flags from a pool register against a constant. + if bits >= 0 and bits <= 4095: + e.code.compareImmediate(Word32, pooled(value), int(bits)) + else: + e.code.loadImmediate(Word32, FastScratch, int64(bits)) + e.code.compareRegister(Word32, pooled(value), FastScratch) + +proc fastLoadWide(e: var Emitter, destination: int, bits: int64) + {.raises: [].} = + ## Loads a sixty-four bit constant into a pool register. + e.code.loadImmediate(Word64, pooled(destination), bits) + +proc fastScaleWide(e: var Emitter, value, tag: int) + {.raises: [BasicError].} = + ## Widens a number to sixty-four bits on the fixed-point scale, its + ## kind read from a register. + let target = pooled(value) + let done = e.label() + e.code.signExtendWord(target, target) + e.jumpIfNotZero(pooled(tag), done) + e.code.shiftLeftImmediate(Word64, target, target, FixedShift) + e.place(done) + +proc fastAnswer(e: var Emitter, destination: int, check: Check) + {.raises: [].} = + ## Writes BASIC's -1 for true and zero for false. + e.code.setOnCondition(Word32, pooled(destination), + nativeCondition(check)) + +proc fastJumpIfZero(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a pool register holds zero. + e.jumpIfZero(pooled(value), target) + +proc fastJumpIfNotZero(e: var Emitter, value: int, target: Label) + {.raises: [].} = + ## Jumps when a pool register holds anything but zero. + e.jumpIfNotZero(pooled(value), target) + +proc fastJumpIfDiffer(e: var Emitter, left, right: int, target: Label) + {.raises: [].} = + ## Jumps when two pool registers differ. + e.code.compareRegister(Word32, pooled(left), pooled(right)) + e.jumpWhen(NotEqualCondition, target) + +proc fastCellAddress(e: var Emitter, index: int, extent: ArrayExtent, + deopt: Label) {.raises: [BasicError].} = + ## Bounds checks an index held in the pool, leaving it untouched, and + ## leaves the cell's address in Cell. + let position = pooled(index) + e.code.loadImmediate(Word32, FastScratch, int64(extent.length)) + e.code.compareRegister(Word32, position, FastScratch) + e.jumpWhen(CarrySetCondition, deopt) + e.code.loadImmediate(Word32, FastScratch, int64(extent.base)) + e.code.addRegister(Word32, FastScratch, FastScratch, position) + e.code.addRegister(Word64, Cell, MemoryBase, FastScratch, 4) + +## Strings read in place +## +## A string value is its storage's owner in the high half and a handle +## into the span table in the low half. Everything is checked just as +## the interpreter checks it, and anything amiss takes the slow path, +## which raises the interpreter's own error. + +proc stringSpan(e: var Emitter, place: Place, reference, start, + length: Register, slow: Label) {.raises: [BasicError].} = + ## Reads a string's start and length, or takes the slow path unless + ## the value is a string of this storage's current generation. + let (base, offset) = e.reach(place) + e.code.loadByte(start, base, offset) + e.code.compareImmediate(Word32, start, StringTag) + e.jumpWhen(NotEqualCondition, slow) + e.code.loadDouble(reference, base, offset + ValuePayload) + e.code.loadDouble(length, Context, ContextStringOwner) + e.code.loadWord(length, length, 0) + e.jumpIfZero(length, slow) + e.code.shiftRightImmediate(Word64, start, reference, 32) + e.code.compareRegister(Word32, start, length) + e.jumpWhen(NotEqualCondition, slow) + e.code.loadDouble(length, Context, ContextStringSpans) + e.code.loadDouble(start, length, 0) + e.code.moveRegister(Word32, reference, reference) + e.code.compareRegister(Word64, reference, start) + e.jumpWhen(CarrySetCondition, slow) + e.code.loadDouble(length, length, 8) + e.code.addRegister(Word64, length, length, reference, 3) + e.code.loadWord(start, length, 8) + e.code.loadWord(length, length, 12) + +proc chargeWork(e: var Emitter, cost: Register, slow: Label) + {.raises: [].} = + ## Charges work worked out at run time, or takes the slow path when it + ## cannot be afforded, where the interpreter raises. + e.code.compareRegister(Word64, Work, cost) + e.jumpWhen(LessCondition, slow) + e.code.subtractRegister(Word64, Work, Work, cost) + +proc arenaBase(e: var Emitter, destination: Register) + {.raises: [BasicError].} = + ## Points at the first byte of the string arena. + e.code.loadDouble(destination, Context, ContextStringArena) + e.code.loadDouble(destination, destination, 8) + e.code.addImmediate(Word64, destination, destination, 8) + +proc stringFunction(e: var Emitter, function: TextFunction, slow: Label) + {.raises: [BasicError].} = + ## Answers LEN or ASC of the first argument into the first working + ## register, charging what the interpreter charges. + e.stringSpan(argument(0), x0, x1, x2, slow) + if function == CodeFunction: + e.jumpIfZero(x2, slow) + e.code.addImmediate(Word64, x3, x2, 1) + e.chargeWork(x3, slow) + if function == LengthFunction: + e.code.moveRegister(Word32, temp(0), x2) + else: + e.arenaBase(x4) + e.code.addRegister(Word64, x4, x4, x1) + e.code.loadByte(temp(0), x4, 0) + +proc stringEquality(e: var Emitter, left, right: Place, equal: bool, + slow: Label) {.raises: [BasicError].} = + ## Answers whether two strings hold the same bytes, into the first + ## working register, charging both lengths as the interpreter does. + e.stringSpan(left, x0, x1, x2, slow) + e.stringSpan(right, x3, x4, x5, slow) + e.code.addRegister(Word64, x6, x2, x5) + e.chargeWork(x6, slow) + let differ = e.label() + let same = e.label() + let done = e.label() + e.code.compareRegister(Word32, x2, x5) + e.code.branchIf(NotEqualCondition, differ) + e.arenaBase(x6) + e.code.addRegister(Word64, x1, x6, x1) + e.code.addRegister(Word64, x4, x6, x4) + e.code.branchIfZero(Word32, x2, same) + let again = e.label() + e.place(again) + e.code.loadByte(x7, x1, 0) + e.code.loadByte(x8, x4, 0) + e.code.compareRegister(Word32, x7, x8) + e.code.branchIf(NotEqualCondition, differ) + e.code.addImmediate(Word64, x1, x1, 1) + e.code.addImmediate(Word64, x4, x4, 1) + e.code.subtractImmediate(Word32, x2, x2, 1) + e.code.branchIfNotZero(Word32, x2, again) + e.place(same) + e.code.loadImmediate(Word32, temp(0), if equal: -1 else: 0) + e.jump(done) + e.place(differ) + e.code.loadImmediate(Word32, temp(0), if equal: 0 else: -1) + e.place(done) + +proc jumpUnlessTag(e: var Emitter, tag: int, value: int, target: Label) + {.raises: [BasicError].} = + ## Jumps unless a working register holds one particular tag. + e.code.compareImmediate(Word32, temp(tag), value) + e.jumpWhen(NotEqualCondition, target) + +proc halt(e: var Emitter, offset: int32) {.raises: [BasicError].} = + ## Publishes the budgets and where the program stopped, then returns. + e.code.storeDouble(Instructions, Context, ContextInstructions) + e.code.storeDouble(Work, Context, ContextWork) + e.code.loadImmediate(Word32, temp(0), int64(offset)) + e.code.storeWord(temp(0), Context, ContextOffset) + e.epilogue(NativeCompleted) + +proc finish(e: var Emitter): seq[byte] {.raises: [BasicError].} = + ## Resolves every branch and returns the finished bytes. + e.code.resolve() + result = newSeq[byte](e.code.code.len * 4) + if result.len > 0: + copyMem(result[0].addr, e.code.code[0].addr, result.len) + +proc offsetBytes(e: Emitter, target: Label): int {.raises: [].} = + ## Returns where a label ended up, in bytes. + e.code.offsetOf(target) * 4 diff --git a/src/bassy/bytecode.nim b/src/bassy/bytecode.nim new file mode 100644 index 0000000..136aae0 --- /dev/null +++ b/src/bassy/bytecode.nim @@ -0,0 +1,75 @@ +## Defines the register bytecode shared by the interpreter and the +## native compiler. Opcodes carry up to three operands, whose meaning +## depends on the operation. + +type + Op* = enum + MeterOp, + LoadImmediateOp, + LoadFixedOp, + LoadStringOp, + TextCallOp, + MoveOp, + LoadGlobalOp, + LoadHostDataOp, + StoreGlobalOp, + StoreGlobalImmediateOp, + MoveGlobalOp, + AddGlobalImmediateOp, + AddGlobalOp, + AddGlobalHostDataOp, + AddGlobalRegisterOp, + ModuloGlobalImmediateOp, + AddGlobalArrayGlobalIndexOp, + AddOp, + SubtractOp, + MultiplyOp, + DivideOp, + IntegerDivideOp, + ModuloOp, + NegateOp, + EqualOp, + NotEqualOp, + LessOp, + LessEqualOp, + GreaterOp, + GreaterEqualOp, + AndOp, + OrOp, + XorOp, + EqvOp, + ImpOp, + NotOp, + JumpOp, + JumpIfZeroOp, + JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp, + JumpUnlessGlobalModuloEqualZeroOp, + ArrayGetOp, + ArraySetOp, + BufferGetOp, + BufferSetOp, + ArrayAddGlobalsOp, + SetArgumentOp, + SetArgumentImmediateOp, + SetArgumentGlobalOp, + HostCallOp, + CallOp, + GosubOp, + ReturnOp, + ReturnLabelOp, + ExitSubOp, + HaltOp, + PrintTextOp, + PrintValueOp, + PrintNewlineOp + + Instruction* = object + op*: Op + a*: int32 + b*: int32 + c*: int32 diff --git a/src/bassy/jit.nim b/src/bassy/jit.nim new file mode 100644 index 0000000..4fadd07 --- /dev/null +++ b/src/bassy/jit.nim @@ -0,0 +1,1863 @@ +## Compiles a whole program from the register bytecode to machine code. +## +## Every bytecode offset becomes a native block, so jumps, calls and +## returns go straight from one block to the next and the interpreter +## loop never runs. Values stay exactly where the interpreter keeps them, +## in the globals, the register file, the arguments and the array cells, +## which is what lets the two agree on every result, every budget and +## every failure. +## +## The common cases run inline: whole-number and fixed-point arithmetic, +## comparisons, branches, moves, array cells, calls, returns and budget +## meters. Everything else, such as strings, printing and host functions, +## and every operation about to fail, calls the one routine the +## interpreter itself runs for that instruction. Nothing is written +## before such a call, so the instruction is simply run there instead. +## +## The walker below is written once. Each architecture supplies the same +## small set of emitters, so AArch64 and x86-64 stay in step. + +import + bytecode, machine, numbers, texts + +export machine.jitSupported, machine.enterCompiledCode, + machine.leaveCompiledCode, machine.runningCompiledCode + +const + NativeArm64* = NativeCode and defined(arm64) + NativeAmd64* = NativeCode and defined(amd64) + +when NativeArm64: + import arm64 +elif NativeAmd64: + import amd64 + +type + NativeStatus* = enum + ## Why compiled code returned to its caller. + NativeCompleted, + NativeFailed, + NativeRetired + + NativeContext* = object + ## The interpreter state compiled code reads and writes. The frame and + ## the budgets live here whenever the interpreter's own code may need + ## them, and go back into the runtime once compiled code returns. + globals*: pointer + remainingInstructions*: int64 + remainingWork*: int64 + pc*: int32 + memory*: pointer + hostData*: pointer + frames*: pointer + arguments*: pointer + registerFile*: pointer + table*: pointer + base*: int32 + depth*: int32 + routine*: int32 + runtime*: pointer + step*: pointer + hostStep*: pointer + stringOwner*: pointer + stringSpans*: pointer + stringArena*: pointer + + NativeCall* = proc(context: ptr NativeContext): int32 + {.cdecl, gcsafe, raises: [].} + + RoutineExtent* = object + ## Where one routine's code sits, how many slots a call to it needs, + ## and how many of those the caller fills in. + entry*: int32 + length*: int32 + registers*: int32 + parameters*: int32 + + CallLimits* = object + ## The two ceilings a call has to respect, read from the runtime. + frames*: int32 + slots*: int32 + + ArrayExtent* = object + ## Where one array sits in the shared cell storage, and how long it is. + base*: int32 + length*: int32 + +const + ValueStride = 16 + ValuePayload = 8 + ContextInstructions = 8 + ContextWork = 16 + ContextOffset = 24 + ContextMemory = 32 + ContextHostData = 40 + ContextFrames = 48 + ContextArguments = 56 + ContextRegisterFile = 64 + ContextTable = 72 + ContextBase = 80 + ContextDepth = 84 + ContextRoutine = 88 + ContextRuntime = 96 + ContextStep = 104 + ContextHostStep = 112 + ContextStringOwner = 120 + ContextStringSpans = 128 + ContextStringArena = 136 + + ## One frame as the interpreter lays it out: where the caller's slots + ## start, which routine it was in, where to carry on, and whether it + ## came from a call or from a GOSUB. The host checks these against the + ## real thing before any of it is compiled. + FrameStride* = 16 + FrameBase* = 0 + FrameRoutine* = 4 + FrameReturn* = 8 + FrameTag* = 12 + + FixedTag = 1 + StringTag = 2 + FixedShift = 16 + FixedRounding = 1'i64 shl (FixedShift - 1) + + ## Fixed-point values are only modelled inline when overflow is allowed + ## to wrap. Under fixedChecks the interpreter asserts instead, so that + ## build hands every fixed-point operation to the interpreter's code. + ModelsFixed* = not defined(fixedChecks) + + MaxProgramBytes = 64 * 1024 * 1024 + ## How many blocks one fast version carries on into, holding values. + MaxChain = 4 + ## Leaving a routine without one of these would run on into the next + ## routine's code, which no call set up. + Terminators = {JumpOp, ReturnOp, ReturnLabelOp, ExitSubOp, HaltOp} + +proc layoutMatches*(): bool {.raises: [].} = + ## Confirms the memory layout the code generator writes by hand. + ## + ## Generated code reaches into values and into the context by fixed + ## offsets worked out from this Nim version. Nothing guarantees those + ## stay put, and a silent change would turn every compiled store into a + ## write at the wrong address, so they are checked rather than trusted. + if sizeof(Value) != ValueStride: + return false + # The values are built in a sequence and read through copyMem rather + # than cast from locals. Nim 2.2.6 and 2.2.10 both fail to compile a + # procedure that takes the address of a converter-initialised variant + # local and also returns early, with an index error and no location. + var probe = newSeq[Value](2) + probe[0] = toValue(0x5A6B7C0D'i32) + probe[1] = toValue(fixed(1'i32)) + var image: array[ValueStride * 2, byte] + copyMem(image[0].addr, probe[0].addr, ValueStride * 2) + # A whole number must be tagged zero and a fixed-point one tagged one, + # because the generated code tests the tag byte for exactly those. + if image[0] != byte(ord(IntegerValue)) or ord(IntegerValue) != 0: + return false + if image[ValueStride] != byte(FixedTag) or ord(FixedValue) != FixedTag or + ord(StringValue) != StringTag: + return false + var payload = 0'i32 + copyMem(payload.addr, image[ValuePayload].addr, sizeof(int32)) + if payload != 0x5A6B7C0D'i32: + return false + copyMem(payload.addr, image[ValueStride + ValuePayload].addr, + sizeof(int32)) + if payload != int32(fixed(1'i32)): + return false + var context: NativeContext + let origin = cast[int](context.addr) + template at(field: untyped): int = + cast[int](context.field.addr) - origin + at(remainingInstructions) == ContextInstructions and + at(remainingWork) == ContextWork and + at(pc) == ContextOffset and + at(memory) == ContextMemory and + at(hostData) == ContextHostData and + at(frames) == ContextFrames and + at(arguments) == ContextArguments and + at(registerFile) == ContextRegisterFile and + at(table) == ContextTable and + at(base) == ContextBase and + at(depth) == ContextDepth and + at(routine) == ContextRoutine and + at(runtime) == ContextRuntime and + at(step) == ContextStep and + at(hostStep) == ContextHostStep and + at(stringOwner) == ContextStringOwner and + at(stringSpans) == ContextStringSpans and + at(stringArena) == ContextStringArena + +type + Machine* = ref object + ## One whole program compiled to machine code. + size*: int + listing*: seq[byte] + table: seq[pointer] + buffer: CodeBuffer + call: NativeCall + + Home = enum + ## Where a value lives. + SlotHome, + GlobalHome, + ArgumentHome, + HostHome, + CellHome + + Place = object + ## One value's address, as a home and an index into it. + home: Home + index: int32 + + Branching = enum + ## Where a slow path carries on once the interpreter's code has run. + ToNext, + ToOffset + + Check = enum + ## An architecture-neutral comparison outcome. + EqualCheck, + NotEqualCheck, + LessCheck, + LessEqualCheck, + GreaterCheck, + GreaterEqualCheck + +when NativeArm64 or NativeAmd64: + type + Stub = object + ## A slow path, placed after the block its operation sits in. + label: Label + offset: int32 + carry: Branching + +proc slot(index: int32): Place {.inline, raises: [].} = + ## Names a register slot in the current frame. + Place(home: SlotHome, index: index) + +proc global(index: int32): Place {.inline, raises: [].} = + ## Names a scalar global. + Place(home: GlobalHome, index: index) + +proc argument(index: int32): Place {.inline, raises: [].} = + ## Names a staged call argument. + Place(home: ArgumentHome, index: index) + +proc host(index: int32): Place {.inline, raises: [].} = + ## Names a host data value. + Place(home: HostHome, index: index) + +proc cell(): Place {.inline, raises: [].} = + ## Names the array cell whose address was just worked out. + Place(home: CellHome) + +proc comparisonCheck(op: Op): Check {.raises: [].} = + ## Returns the outcome a comparison answers true on. + case op + of EqualOp: EqualCheck + of NotEqualOp: NotEqualCheck + of LessOp: LessCheck + of LessEqualOp: LessEqualCheck + of GreaterOp: GreaterCheck + else: GreaterEqualCheck + +proc swapped(check: Check): Check {.raises: [].} = + ## Returns the outcome that asks the same with the operands turned round. + case check + of LessCheck: GreaterCheck + of LessEqualCheck: GreaterEqualCheck + of GreaterCheck: LessCheck + of GreaterEqualCheck: LessEqualCheck + else: check + +proc takenOn(op: Op): Check {.raises: [].} = + ## Returns the outcome on which a fused test takes its branch. + case op + of JumpUnlessGlobalEqualImmediateOp: NotEqualCheck + of JumpUnlessGlobalNotEqualImmediateOp: EqualCheck + of JumpUnlessGlobalLessImmediateOp: GreaterEqualCheck + of JumpUnlessGlobalLessEqualImmediateOp: GreaterCheck + of JumpUnlessGlobalGreaterImmediateOp: LessEqualCheck + else: LessCheck + +proc lowBitCount(divisor: int32): int {.raises: [].} = + ## Returns how many low bits decide divisibility when the divisor is a + ## power of two, or of its negation, and zero otherwise. Truncating + ## division leaves nothing over exactly when those bits are clear, for + ## negative dividends as well, so a bit test can stand in for a divide. + var magnitude = abs(int64(divisor)) + if magnitude < 2 or (magnitude and (magnitude - 1)) != 0: + return 0 + while magnitude > 1: + magnitude = magnitude shr 1 + inc result + +when NativeArm64: + include arm64jit +elif NativeAmd64: + include amd64jit + +proc invoke*(machine: Machine, context: var NativeContext): NativeStatus + {.raises: [].} = + ## Runs the compiled program from the offset the context names. + NativeStatus(machine.call(context.addr)) + +type + Kind = enum + ## What the compiler knows of a held value's kind. + UnknownKind, + WholeKind, + FixedKind + + Held = object + ## A slot or global a block's fast version keeps in registers. Its + ## kind is either known here or held in a register of its own, and it + ## is always a number: anything else never gets into a register. + place: Place + payload: int + tag: int + kind: Kind + dirty: bool + constant: bool + value: int32 + + Loop = object + ## A loop whose globals can live in registers while it runs. + ## + ## Its budget is looked at only at checkpoints: its head and every + ## other backward-branch target in it. Between two checkpoints a path + ## has no backward branch, so it meets each meter at most once, and so + ## charges at most what every meter in the loop adds up to. Each look + ## asks for that much, and a loop short of it goes on in general code, + ## which looks at every block and so refuses at the very same one. + start: int + stop: int + globals: seq[int32] + checkpoints: seq[bool] + passInstructions: int64 + passWork: int64 + +proc loopGlobals(item: Instruction, globals: var seq[int32]) + {.raises: [].} = + ## Records every global one operation reads or writes. + template note(index: int32) = + if index notin globals: + globals.add(index) + case item.op + of StoreGlobalImmediateOp, AddGlobalImmediateOp, + JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp, + JumpUnlessGlobalModuloEqualZeroOp, + AddGlobalHostDataOp, AddGlobalRegisterOp, StoreGlobalOp: + note(item.a) + of MoveGlobalOp, AddGlobalOp, ModuloGlobalImmediateOp: + note(item.a) + note(item.b) + of LoadGlobalOp: + note(item.b) + of AddGlobalArrayGlobalIndexOp: + note(item.a) + note(item.c) + of ArrayAddGlobalsOp: + note(item.b) + note(item.c) + else: + discard + +proc fitsLoop(item: Instruction): bool {.raises: [].} = + ## Reports whether an operation can run with its globals in registers. + ## Nothing that calls out may, since the interpreter's code would find + ## the globals' memory stale, and nothing that leaves the loop's code by + ## any way but a branch may either. + case item.op + of MeterOp, LoadImmediateOp, LoadFixedOp, MoveOp, LoadGlobalOp, + LoadHostDataOp, StoreGlobalOp, StoreGlobalImmediateOp, MoveGlobalOp, + AddGlobalImmediateOp, AddGlobalOp, AddGlobalHostDataOp, + AddGlobalRegisterOp, AddGlobalArrayGlobalIndexOp, ArrayAddGlobalsOp, + AddOp, SubtractOp, MultiplyOp, IntegerDivideOp, ModuloOp, NegateOp, + EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, GreaterEqualOp, + AndOp, OrOp, XorOp, EqvOp, ImpOp, NotOp, JumpOp, JumpIfZeroOp, + JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp, + ArrayGetOp, ArraySetOp: + true + of DivideOp: + ModelsFixed + of ModuloGlobalImmediateOp: + item.c != 0 + of JumpUnlessGlobalModuloEqualZeroOp: + item.b != 0 + else: + false + +proc findLoops(code: seq[Instruction], capacity: int): seq[Loop] + {.raises: [].} = + ## Picks the loops worth specialising: each closed by a jump back to its + ## head, made only of operations that fit, and touching no more globals + ## than there are registers. Outer loops are tried first, and a loop + ## inside one already taken runs inside that one's registers. + var candidates: seq[(int, int)] + for index, item in code: + if item.op == JumpOp and int(item.a) <= index and item.a >= 0: + candidates.add((int(item.a), index + 1)) + var covered = newSeq[bool](code.len) + while candidates.len > 0: + var widest = 0 + for position in 1 ..< candidates.len: + let (start, stop) = candidates[position] + if stop - start > candidates[widest][1] - candidates[widest][0]: + widest = position + let (start, stop) = candidates[widest] + candidates.delete(widest) + var fits = true + var globals: seq[int32] + for index in start ..< stop: + if covered[index] or not code[index].fitsLoop: + fits = false + break + code[index].loopGlobals(globals) + if not fits or globals.len == 0 or globals.len > capacity: + continue + var loop = Loop(start: start, stop: stop, globals: globals, + checkpoints: newSeq[bool](stop - start)) + loop.checkpoints[0] = true + for index in start ..< stop: + let item = code[index] + var target = -1 + case item.op + of JumpOp: target = int(item.a) + of JumpIfZeroOp: target = int(item.b) + of JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp, + JumpUnlessGlobalModuloEqualZeroOp: target = int(item.c) + else: discard + if target >= start and target <= index: + loop.checkpoints[target - start] = true + if item.op == MeterOp: + loop.passInstructions += int64(item.b) + loop.passWork += int64(item.a) + for offset, wanted in loop.checkpoints: + if wanted and code[start + offset].op != MeterOp: + fits = false + if not fits: + continue + for index in start ..< stop: + covered[index] = true + result.add(loop) + +proc emitProgram(code: seq[Instruction], routines: seq[RoutineExtent], + ownerOf: seq[int32], extents: seq[ArrayExtent], constants: seq[int32], + limits: CallLimits, far, strings: bool): (seq[byte], seq[int]) + {.raises: [BasicError].} = + ## Emits the whole program and returns its bytes along with where each + ## offset's block starts. + ## + ## Every offset has general code, which keeps every value in memory and + ## so can hand any instruction to the interpreter's code. A loop that + ## fits also gets a second, specialised copy that keeps its globals in + ## registers. Entering the loop's head checks they hold whole numbers + ## and moves into the copy; anything the copy does not expect writes the + ## registers back and carries on in the general code of that very + ## instruction, which does it the ordinary way. + when not (NativeArm64 or NativeAmd64): + raise newException(BasicError, "BASIC has no whole-program backend here") + else: + var e = Emitter(far: far) + var blocks = newSeq[Label](code.len + 1) + var general = newSeq[Label](code.len + 1) + for index in 0 .. code.len: + blocks[index] = e.label() + general[index] = blocks[index] + e.limit = code.len + e.outside = blocks[code.len] + let dispatchLabel = e.label() + let slowLabel = e.label() + let hostLabel = e.label() + let failedLabel = e.label() + + let loops = findLoops(code, Hoisting.len) + var loopAt = newSeq[int](code.len) + for index in 0 ..< code.len: + loopAt[index] = -1 + for number, loop in loops: + loopAt[loop.start] = number + general[loop.start] = e.label() + # Every ordinary block starts at a meter, and its general version gets + # a label of its own, since its entry is the fast version. + var inLoop = newSeq[bool](code.len) + for loop in loops: + for index in loop.start ..< loop.stop: + inLoop[index] = true + for index in 0 ..< code.len: + if code[index].op == MeterOp and not inLoop[index]: + general[index] = e.label() + + e.prologue() + e.jump(dispatchLabel) + + var stubs: seq[Stub] + + template emitInstruction(at: int, specialised: static bool, + loop: Loop, inside: seq[Label], exits: var seq[(Label, int, bool)]) = + ## Emits one instruction, in general code or inside a loop's copy. + let item = code[at] + let offset = int32(at) + + template slowFor(after: Branching): Label = + ## Names where this instruction goes when it cannot run inline. + when specialised: + leaveFor(at, true) + else: + let stub = Stub(label: e.label(), offset: offset, carry: after) + stubs.add(stub) + stub.label + + template leaveFor(target: int, again: bool): Label = + ## Names a stub that writes the loop's registers back and goes on + ## in general code: the same instruction again, or a branch target. + var found = -1 + for position, exit in exits: + if exit[1] == target and exit[2] == again: + found = position + if found < 0: + exits.add((e.label(), target, again)) + found = exits.len - 1 + exits[found][0] + + template toBlock(target: int32): Label = + ## Names where a branch to an offset lands. + when specialised: + if int(target) >= loop.start and int(target) < loop.stop: + inside[int(target) - loop.start] + else: + leaveFor(int(target), false) + else: + blocks[int(target)] + + template held(which: int32): int {.used.} = + ## Returns which register holds a global inside this loop. + loop.globals.find(which) + + template runSlow() = + ## Runs this instruction through the interpreter's code in line. + e.callSlow(offset, slowLabel) + + var fallsThrough {.used.} = true + case item.op + of MeterOp: + when specialised: + if loop.checkpoints[at - loop.start]: + e.meter(item.b, item.a, slowFor(ToNext), + loop.passInstructions, loop.passWork) + else: + e.charge(item.b, item.a) + else: + e.meter(item.b, item.a, slowFor(ToNext)) + of LoadImmediateOp: + e.writeConstant(slot(item.a), 0, item.b) + of LoadFixedOp: + e.writeConstant(slot(item.a), FixedTag, constants[int(item.b)]) + of MoveOp: + e.copyValue(slot(item.a), slot(item.b)) + of LoadGlobalOp: + when specialised: + e.hoistedToTemp(0, held(item.b)) + e.writeWhole(slot(item.a), 0) + else: + e.copyValue(slot(item.a), global(item.b)) + of LoadHostDataOp: + e.copyValue(slot(item.a), host(item.b)) + of StoreGlobalOp: + when specialised: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessWhole(2, slow) + e.tempToHoisted(held(item.a), 0) + else: + e.copyValue(global(item.a), slot(item.b)) + of StoreGlobalImmediateOp: + when specialised: + e.setHoisted(held(item.a), item.b) + else: + e.writeConstant(global(item.a), 0, item.b) + of MoveGlobalOp: + when specialised: + e.copyHoisted(held(item.a), held(item.b)) + else: + e.copyValue(global(item.a), global(item.b)) + of SetArgumentOp: + e.copyValue(argument(item.a), slot(item.b)) + of SetArgumentImmediateOp: + e.writeConstant(argument(item.a), 0, item.b) + of SetArgumentGlobalOp: + e.copyValue(argument(item.a), global(item.b)) + of AddGlobalImmediateOp: + when specialised: + e.addHoistedConstant(held(item.a), item.b) + else: + let slow = slowFor(ToNext) + e.readValue(0, 2, global(item.a)) + e.unlessWhole(2, slow) + e.loadConstant(1, item.b) + e.add(0, 1) + e.writeWhole(global(item.a), 0) + of AddGlobalOp, AddGlobalHostDataOp, AddGlobalRegisterOp: + when specialised: + if item.op == AddGlobalOp: + e.addHoisted(held(item.a), held(item.b)) + else: + let slow = slowFor(ToNext) + let source = + if item.op == AddGlobalHostDataOp: host(item.b) + else: slot(item.b) + e.readValue(1, 3, source) + e.unlessWhole(3, slow) + e.addTempToHoisted(held(item.a), 1) + else: + let slow = slowFor(ToNext) + let source = + case item.op + of AddGlobalOp: global(item.b) + of AddGlobalHostDataOp: host(item.b) + else: slot(item.b) + e.readValue(0, 2, global(item.a)) + e.unlessWhole(2, slow) + e.readValue(1, 3, source) + e.unlessWhole(3, slow) + e.add(0, 1) + e.writeWhole(global(item.a), 0) + of ModuloGlobalImmediateOp: + if item.c == 0: + runSlow() + else: + when specialised: + e.hoistedToTemp(0, held(item.b)) + e.loadConstant(1, item.c) + e.remainder(0, 1) + e.tempToHoisted(held(item.a), 0) + else: + let slow = slowFor(ToNext) + e.readValue(0, 2, global(item.b)) + e.unlessWhole(2, slow) + e.loadConstant(1, item.c) + e.remainder(0, 1) + e.writeWhole(global(item.a), 0) + of AddGlobalArrayGlobalIndexOp: + let slow = slowFor(ToNext) + when specialised: + e.hoistedToTemp(0, held(item.c)) + e.cellAddress(0, extents[int(item.b)], slow) + e.readValue(1, 3, cell()) + e.unlessWhole(3, slow) + e.addTempToHoisted(held(item.a), 1) + else: + e.readValue(0, 2, global(item.c)) + e.unlessWhole(2, slow) + e.cellAddress(0, extents[int(item.b)], slow) + e.readValue(1, 3, cell()) + e.unlessWhole(3, slow) + e.readValue(0, 2, global(item.a)) + e.unlessWhole(2, slow) + e.add(0, 1) + e.writeWhole(global(item.a), 0) + of ArrayAddGlobalsOp: + let slow = slowFor(ToNext) + when specialised: + e.hoistedToTemp(0, held(item.b)) + e.cellAddress(0, extents[int(item.a)], slow) + e.readValue(1, 3, cell()) + e.unlessWhole(3, slow) + e.hoistedToTemp(0, held(item.c)) + else: + e.readValue(0, 2, global(item.b)) + e.unlessWhole(2, slow) + e.cellAddress(0, extents[int(item.a)], slow) + e.readValue(1, 3, cell()) + e.unlessWhole(3, slow) + e.readValue(0, 2, global(item.c)) + e.unlessWhole(2, slow) + e.add(1, 0) + e.writeWhole(cell(), 1) + of AddOp, SubtractOp, MultiplyOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.readValue(1, 3, slot(item.c)) + when ModelsFixed: + # A whole number beside a fixed-point one becomes fixed point + # first, exactly as the interpreter promotes it, and the answer + # is fixed point. + let ready = e.label() + let promoteRight = e.label() + e.unlessNumeric(2, slow) + e.unlessNumeric(3, slow) + e.jumpIfSame(2, 3, ready) + e.whenFixed(2, promoteRight) + e.toFixed(0, slow) + e.loadConstant(2, FixedTag) + e.jump(ready) + e.place(promoteRight) + e.toFixed(1, slow) + e.place(ready) + else: + e.unlessSame(2, 3, slow) + e.unlessWhole(2, slow) + case item.op + of AddOp: + e.add(0, 1) + of SubtractOp: + e.subtract(0, 1) + else: + when ModelsFixed: + let fixedWay = e.label() + let joined = e.label() + e.whenFixed(2, fixedWay) + e.multiply(0, 1) + e.jump(joined) + e.place(fixedWay) + e.multiplyFixed(0, 1) + e.place(joined) + else: + e.multiply(0, 1) + e.writeKind(slot(item.a), 2, 0) + of DivideOp: + when ModelsFixed: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessNumeric(2, slow) + e.readValue(1, 3, slot(item.c)) + e.unlessNumeric(3, slow) + e.widenToFixed(0, 2, slow) + e.widenToFixed(1, 3, slow) + e.divideFixed(0, 1, slow) + e.writeFixed(slot(item.a), 0) + else: + runSlow() + of IntegerDivideOp, ModuloOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessWhole(2, slow) + e.readValue(1, 3, slot(item.c)) + e.unlessWhole(3, slow) + e.jumpIfZeroValue(1, slow) + if item.op == IntegerDivideOp: + e.quotient(0, 1) + else: + e.remainder(0, 1) + e.writeWhole(slot(item.a), 0) + of NegateOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + when ModelsFixed: + e.unlessNumeric(2, slow) + else: + e.unlessWhole(2, slow) + e.negate(0) + e.writeKind(slot(item.a), 2, 0) + of EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, + GreaterEqualOp: + # The same kind on both sides orders the same on the stored bits, + # and the answer is always a whole number. + let slow = slowFor(ToNext) + let answered = e.label() + e.readValue(0, 2, slot(item.b)) + e.readValue(1, 3, slot(item.c)) + when not specialised: + if strings and item.op in {EqualOp, NotEqualOp}: + # Two strings compare byte by byte where they are kept. Inside + # a specialised loop this would need registers its globals + # hold, so there it leaves the loop instead. + let numbers = e.label() + e.jumpUnlessTag(2, StringTag, numbers) + e.jumpUnlessTag(3, StringTag, numbers) + e.stringEquality(slot(item.b), slot(item.c), + item.op == EqualOp, slow) + e.jump(answered) + e.place(numbers) + e.unlessNumeric(2, slow) + e.unlessNumeric(3, slow) + let sameKind = e.label() + let decided = e.label() + e.jumpIfSame(2, 3, sameKind) + # Kinds that differ compare on the widened fixed-point scale, + # where every value of either kind has an exact place. + e.scaleWide(0, 2) + e.scaleWide(1, 3) + e.compareWide(0, 1) + e.jump(decided) + e.place(sameKind) + e.compare(0, 1) + e.place(decided) + e.answer(0, comparisonCheck(item.op)) + e.place(answered) + e.writeWhole(slot(item.a), 0) + of AndOp, OrOp, XorOp, EqvOp, ImpOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessWhole(2, slow) + e.readValue(1, 3, slot(item.c)) + e.unlessWhole(3, slow) + case item.op + of AndOp: + e.bitAnd(0, 1) + of OrOp: + e.bitOr(0, 1) + of XorOp: + e.bitXor(0, 1) + of EqvOp: + e.bitXor(0, 1) + e.bitNot(0) + else: + e.bitNot(0) + e.bitOr(0, 1) + e.writeWhole(slot(item.a), 0) + of NotOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessWhole(2, slow) + e.bitNot(0) + e.writeWhole(slot(item.a), 0) + of JumpOp: + e.jump(toBlock(item.a)) + fallsThrough = false + of JumpIfZeroOp: + # A fixed-point zero is all zero bits too, so either kind tests + # the same way. + let slow = slowFor(ToOffset) + e.readValue(0, 2, slot(item.a)) + e.unlessNumeric(2, slow) + e.jumpIfZeroValue(0, toBlock(item.b)) + of JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp: + when specialised: + e.compareHoisted(held(item.a), item.b) + else: + let slow = slowFor(ToOffset) + let whole = e.label() + let decided = e.label() + e.readValue(0, 2, global(item.a)) + e.whenWhole(2, whole) + # A fixed-point global meets the constant on the widened scale. + e.unlessFixed(2, slow) + e.scaleWide(0, 2) + e.loadWide(1, int64(item.b) * 65536) + e.compareWide(0, 1) + e.jump(decided) + e.place(whole) + e.compareConstant(0, item.b) + e.place(decided) + e.jumpOn(takenOn(item.op), toBlock(item.c)) + of JumpUnlessGlobalModuloEqualZeroOp: + if item.b == 0: + runSlow() + e.jump(dispatchLabel) + fallsThrough = false + else: + when specialised: + e.hoistedToTemp(0, held(item.a)) + else: + let slow = slowFor(ToOffset) + e.readValue(0, 2, global(item.a)) + e.unlessWhole(2, slow) + let bits = lowBitCount(item.b) + if bits > 0: + e.jumpIfLowBits(0, bits, toBlock(item.c)) + elif item.b != 1 and item.b != -1: + # Dividing by one or minus one leaves nothing over, ever. + e.loadConstant(1, item.b) + e.remainder(0, 1) + e.jumpIfNotZeroValue(0, toBlock(item.c)) + of ArrayGetOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.c)) + e.unlessWhole(2, slow) + e.cellAddress(0, extents[int(item.b)], slow) + e.copyValue(slot(item.a), cell()) + of ArraySetOp: + let slow = slowFor(ToNext) + e.readValue(0, 2, slot(item.b)) + e.unlessWhole(2, slow) + e.cellAddress(0, extents[int(item.a)], slow) + e.copyValue(cell(), slot(item.c)) + of CallOp, GosubOp: + let owner = routines[int(ownerOf[at])] + let slow = slowFor(ToOffset) + if item.op == CallOp: + let callee = routines[int(item.a)] + e.enterRoutine(false, item.a, callee.registers, callee.parameters, + owner.registers, offset + 1, limits, slow) + e.jump(blocks[int(callee.entry)]) + else: + e.enterRoutine(true, ownerOf[at], owner.registers, 0, + owner.registers, offset + 1, limits, slow) + e.jump(blocks[int(item.a)]) + fallsThrough = false + of ReturnOp: + let owner = routines[int(ownerOf[at])] + e.leaveRoutine(owner.parameters, false, slowFor(ToOffset)) + fallsThrough = false + of ExitSubOp: + # With the sub's own frame on top there are no GOSUB frames to + # unwind first, so leaving is a plain return. Anything else is + # left to the interpreter's code, which unwinds them. + e.leaveRoutine(0, true, slowFor(ToOffset)) + fallsThrough = false + of HaltOp: + e.halt(offset) + fallsThrough = false + of ReturnLabelOp: + runSlow() + e.jump(dispatchLabel) + fallsThrough = false + of HostCallOp: + # Host calls have a helper of their own, which goes straight to + # the host call's code instead of through the general dispatch. + e.callSlow(offset, hostLabel) + of TextCallOp: + let function = TextFunction(item.b) + if strings and item.c == 1 and + function in {LengthFunction, CodeFunction}: + # LEN and ASC read the string where it is kept. + let slow = slowFor(ToNext) + e.stringFunction(function, slow) + e.writeWhole(slot(item.a), 0) + else: + runSlow() + of LoadStringOp, PrintTextOp, PrintValueOp, PrintNewlineOp, + BufferGetOp, BufferSetOp: + # Returned arrays live in host buffers, reached only through the + # checked views the interpreter's own code uses. + runSlow() + + when not specialised: + # Slow paths go after the block, out of the way of the fast ones. + let blockEnds = at + 1 == code.len or + code[at + 1].op == MeterOp + if blockEnds and stubs.len > 0: + if fallsThrough: + e.jump(blocks[at + 1]) + for stub in stubs: + e.place(stub.label) + e.callSlow(stub.offset, slowLabel) + case stub.carry + of ToNext: + e.jump(blocks[int(stub.offset) + 1]) + of ToOffset: + e.jump(dispatchLabel) + stubs.setLen(0) + + var noExits: seq[(Label, int, bool)] + var loopCopies: seq[seq[Label]] + for loop in loops: + var inside: seq[Label] + for index in loop.start ..< loop.stop: + inside.add(e.label()) + loopCopies.add(inside) + + ## Fast versions of ordinary blocks + ## + ## An ordinary block, one outside any specialised loop, also gets a + ## fast version that keeps the slots and globals it touches in pool + ## registers from one instruction to the next instead of in memory. A + ## value is read from memory once, written back once before anything + ## else could look, and between the two its kind is checked at most + ## once. Anything the fast version does not expect writes back what + ## was pending and goes on in the block's general version at that very + ## instruction, which does it the ordinary way. + ## + ## Registers are counted rather than guessed at. Before an instruction + ## emits anything that could leave, enough registers are freed for the + ## most it could need; within it none is handed out twice, so a way out + ## taken part way through still finds every pending value where it was. + + var + held: seq[Held] + uses = newSeq[int](Pool.len) + pinned = newSeq[bool](Pool.len) + fastExits: seq[(Label, seq[Held], int)] + + proc samePlace(a, b: Place): bool {.raises: [].} = + ## Reports whether two places name the same value. + a.home == b.home and a.index == b.index + + template kindTag(kind: Kind): int = + ## Returns the tag byte for a known kind. + (if kind == FixedKind: FixedTag else: 0) + + template writeBack(value: Held) = + ## Stores a held value where the interpreter keeps it. + e.fastStore(value.place, value.payload, value.tag, kindTag(value.kind)) + + template forget(position: int) = + ## Drops one held value, freeing its registers once nothing uses them. + dec uses[held[position].payload] + if held[position].tag >= 0: + dec uses[held[position].tag] + held.delete(position) + + template flushAll() = + ## Writes every pending value back, keeping them held. + for position in 0 ..< held.len: + if held[position].dirty: + writeBack(held[position]) + held[position].dirty = false + + template clearAll() = + ## Writes every pending value back and holds nothing further. + flushAll() + held.setLen(0) + for register in 0 ..< Pool.len: + uses[register] = 0 + + template acquire(): int = + ## Hands out a free register for the rest of this instruction. + var found = -1 + for register in 0 ..< Pool.len: + if uses[register] == 0 and not pinned[register]: + found = register + break + if found < 0: + raise newException(BasicError, + "BASIC native compiler ran out of registers") + pinned[found] = true + found + + template pin(register: int) = + ## Keeps a register from being handed out for this instruction. + if register >= 0: + pinned[register] = true + + template findHeld(target: Place): int = + ## Returns where a value is held, or -1. + var found = -1 + for position in 0 ..< held.len: + if samePlace(held[position].place, target): + found = position + found + + template makeRoom(operands: openArray[Place], extra: int) = + ## Frees enough registers for an instruction before it emits anything + ## that could leave: two for each operand not yet held, plus what it + ## needs of its own. Values it reads stay; the oldest others go, + ## clean ones before those that must be written back. + var needed = extra + var counted: seq[Place] + for operand in operands: + var seen = false + for earlier in counted: + if samePlace(earlier, operand): + seen = true + if not seen: + counted.add(operand) + if findHeld(operand) < 0: + needed += 2 + for pass in 0 .. 1: + var position = 0 + while position < held.len: + var free = 0 + for register in 0 ..< Pool.len: + if uses[register] == 0: + inc free + if free >= needed: + break + var keep = false + for operand in operands: + if samePlace(held[position].place, operand): + keep = true + if keep or (pass == 0 and held[position].dirty): + inc position + continue + if held[position].dirty: + writeBack(held[position]) + forget(position) + + template leaveHere(offset: int): Label = + ## Names a way out of the fast version: write back what is pending + ## now, then do this instruction in general code. + var pending: seq[Held] + for value in held: + if value.dirty: + pending.add(value) + let exit = e.label() + fastExits.add((exit, pending, offset)) + exit + + template fetch(target: Place, offset: int): Held = + ## Returns a value in registers, reading it from memory if it is not + ## held yet. Only a number is held; anything else leaves. + var position = findHeld(target) + if position < 0: + let payload = acquire() + let tag = acquire() + e.fastLoad(payload, tag, target, leaveHere(offset)) + held.add(Held(place: target, payload: payload, tag: tag, + kind: UnknownKind)) + inc uses[payload] + inc uses[tag] + position = held.len - 1 + pin(held[position].payload) + pin(held[position].tag) + held[position] + + template bindValue(target: Place, valueRegister, kindRegister: int, + known: Kind, isConstant = false, bits = 0'i32) = + ## Holds a new value for a place, pending until written back, along + ## with what it is when that is known here. + let position = findHeld(target) + if position >= 0: + forget(position) + held.add(Held(place: target, payload: valueRegister, + tag: kindRegister, kind: known, dirty: true, constant: isConstant, + value: bits)) + inc uses[valueRegister] + if kindRegister >= 0: + inc uses[kindRegister] + + template tagOf(value: Held): int = + ## Returns a register holding a value's kind, loading a known kind. + var register = value.tag + if register < 0: + register = acquire() + e.fastConstant(register, int32(kindTag(value.kind))) + register + + template requireWhole(value: Held, target: Place, exit: Label) = + ## Leaves unless a value is a whole number, and from here on knows + ## that it is. + case value.kind + of WholeKind: + discard + of FixedKind: + e.jump(exit) + of UnknownKind: + e.fastJumpIfNotZero(value.tag, exit) + let position = findHeld(target) + if position >= 0 and held[position].tag >= 0: + dec uses[held[position].tag] + held[position].tag = -1 + held[position].kind = WholeKind + + template fallsOnward(op: Op): bool = + ## Reports whether an operation can carry on to the next offset. + op notin {JumpOp, ReturnOp, ReturnLabelOp, ExitSubOp, HaltOp, CallOp, + GosubOp} + + template emitFast(at: int): bool = + ## Emits one instruction into a fast version, or reports that it + ## has no fast form, so that it is done the general way instead. + let item = code[at] + let offset = at + var done = true + case item.op + of MeterOp: + # The budget is looked at exactly as the general code would, and a + # block short of it writes back and goes there to be refused. + e.meter(item.b, item.a, leaveHere(offset)) + of LoadImmediateOp, LoadFixedOp, StoreGlobalImmediateOp: + makeRoom(newSeq[Place](), 1) + let register = acquire() + let kind = + if item.op == LoadFixedOp: FixedKind + else: WholeKind + let bits = + if item.op == LoadFixedOp: constants[int(item.b)] + else: item.b + e.fastConstant(register, bits) + let target = + if item.op == StoreGlobalImmediateOp: global(item.a) + else: slot(item.a) + bindValue(target, register, -1, kind, true, bits) + of MoveOp, LoadGlobalOp, StoreGlobalOp, MoveGlobalOp, LoadHostDataOp: + let (target, source) = + case item.op + of MoveOp: (slot(item.a), slot(item.b)) + of LoadGlobalOp: (slot(item.a), global(item.b)) + of StoreGlobalOp: (global(item.a), slot(item.b)) + of MoveGlobalOp: (global(item.a), global(item.b)) + else: (slot(item.a), host(item.b)) + makeRoom([source], 0) + let value = fetch(source, offset) + bindValue(target, value.payload, value.tag, value.kind, + value.constant, value.value) + of SetArgumentOp, SetArgumentGlobalOp: + let source = + if item.op == SetArgumentOp: slot(item.b) + else: global(item.b) + makeRoom([source], 0) + let value = fetch(source, offset) + e.fastStore(argument(item.a), value.payload, value.tag, + kindTag(value.kind)) + of SetArgumentImmediateOp: + e.writeConstant(argument(item.a), 0, item.b) + of AddGlobalImmediateOp, AddGlobalOp, AddGlobalRegisterOp, + AddGlobalHostDataOp: + let target = global(item.a) + let source = + case item.op + of AddGlobalOp: global(item.b) + of AddGlobalRegisterOp: slot(item.b) + of AddGlobalHostDataOp: host(item.b) + else: target + makeRoom([target, source], 2) + let total = fetch(target, offset) + let exit = leaveHere(offset) + requireWhole(total, target, exit) + let register = acquire() + e.fastMove(register, total.payload) + if item.op == AddGlobalImmediateOp: + let amount = acquire() + e.fastConstant(amount, item.b) + e.fastAdd(register, amount) + else: + let value = fetch(source, offset) + requireWhole(value, source, exit) + e.fastAdd(register, value.payload) + bindValue(target, register, -1, WholeKind) + of AddOp, SubtractOp, MultiplyOp: + when ModelsFixed: + let left = slot(item.b) + let right = slot(item.c) + var known = 0 + for operand in [left, right]: + let position = findHeld(operand) + if position >= 0 and held[position].kind != UnknownKind: + inc known + makeRoom([left, right], 3 + known) + let b = fetch(left, offset) + let c = fetch(right, offset) + template operate(target, source: int, fixed: bool) = + ## Applies this instruction's operation to two registers. + case item.op + of AddOp: + e.fastAdd(target, source) + of SubtractOp: + e.fastSubtract(target, source) + else: + if fixed: + e.fastMultiplyFixed(target, source) + else: + e.fastMultiply(target, source) + template promote(widened: int, side: Held, exit: Label) = + ## Turns a whole number into fixed point in a fresh register. + ## A constant is turned here, and one out of range can only + ## ever leave, as the interpreter would refuse it. + if side.constant: + if side.value >= -32768 and side.value <= 32767: + e.fastConstant(widened, side.value shl FixedShift) + else: + e.jump(exit) + else: + e.fastMove(widened, side.payload) + e.fastToFixed(widened, exit) + if b.kind != UnknownKind and c.kind != UnknownKind: + let result = acquire() + if b.kind == c.kind: + e.fastMove(result, b.payload) + operate(result, c.payload, b.kind == FixedKind) + else: + # Kinds known to differ: the whole side becomes fixed point. + let exit = leaveHere(offset) + let widened = acquire() + if b.kind == WholeKind: + promote(widened, b, exit) + e.fastMove(result, widened) + operate(result, c.payload, true) + else: + promote(widened, c, exit) + e.fastMove(result, b.payload) + operate(result, widened, true) + let kind = + if b.kind == WholeKind and c.kind == WholeKind: WholeKind + else: FixedKind + bindValue(slot(item.a), result, -1, kind) + elif b.kind != UnknownKind or c.kind != UnknownKind: + # One kind is known, so one test of the other decides: the + # same kind goes straight through, the other promotes. + let knownLeft = b.kind != UnknownKind + let known = if knownLeft: b else: c + let unknown = if knownLeft: c else: b + let exit = leaveHere(offset) + let result = acquire() + let widened = acquire() + let otherWay = e.label() + let finished = e.label() + if known.kind == WholeKind: + let resultTag = acquire() + e.fastJumpIfNotZero(unknown.tag, otherWay) + e.fastMove(result, b.payload) + operate(result, c.payload, false) + e.fastConstant(resultTag, 0) + e.jump(finished) + e.place(otherWay) + promote(widened, known, exit) + if knownLeft: + e.fastMove(result, widened) + operate(result, c.payload, true) + else: + e.fastMove(result, b.payload) + operate(result, widened, true) + e.fastConstant(resultTag, FixedTag) + e.place(finished) + bindValue(slot(item.a), result, resultTag, UnknownKind) + else: + e.fastJumpIfZero(unknown.tag, otherWay) + e.fastMove(result, b.payload) + operate(result, c.payload, true) + e.jump(finished) + e.place(otherWay) + e.fastMove(widened, unknown.payload) + e.fastToFixed(widened, exit) + if knownLeft: + e.fastMove(result, b.payload) + operate(result, widened, true) + else: + e.fastMove(result, widened) + operate(result, c.payload, true) + e.place(finished) + bindValue(slot(item.a), result, -1, FixedKind) + else: + let exit = leaveHere(offset) + let leftTag = tagOf(b) + let rightTag = tagOf(c) + let result = acquire() + let resultTag = acquire() + let widened = acquire() + let mixed = e.label() + let promoteRight = e.label() + let joined = e.label() + let finished = e.label() + e.fastJumpIfDiffer(leftTag, rightTag, mixed) + e.fastMove(result, b.payload) + e.fastMove(resultTag, leftTag) + if item.op == MultiplyOp: + let fixedWay = e.label() + e.fastJumpIfNotZero(leftTag, fixedWay) + operate(result, c.payload, false) + e.jump(finished) + e.place(fixedWay) + operate(result, c.payload, true) + else: + operate(result, c.payload, false) + e.jump(finished) + e.place(mixed) + e.fastJumpIfNotZero(leftTag, promoteRight) + e.fastMove(widened, b.payload) + e.fastToFixed(widened, exit) + e.fastMove(result, widened) + operate(result, c.payload, true) + e.jump(joined) + e.place(promoteRight) + e.fastMove(widened, c.payload) + e.fastToFixed(widened, exit) + e.fastMove(result, b.payload) + operate(result, widened, true) + e.place(joined) + e.fastConstant(resultTag, FixedTag) + e.place(finished) + bindValue(slot(item.a), result, resultTag, UnknownKind) + else: + done = false + of NegateOp: + when ModelsFixed: + makeRoom([slot(item.b)], 1) + let value = fetch(slot(item.b), offset) + let result = acquire() + e.fastMove(result, value.payload) + e.fastNegate(result) + bindValue(slot(item.a), result, value.tag, value.kind) + else: + done = false + of EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, + GreaterEqualOp: + let left = slot(item.b) + let right = slot(item.c) + var known = 0 + for operand in [left, right]: + let position = findHeld(operand) + if position >= 0 and held[position].kind != UnknownKind: + inc known + makeRoom([left, right], 3 + known) + let b = fetch(left, offset) + let c = fetch(right, offset) + var check = comparisonCheck(item.op) + if (c.constant or b.constant) and not (b.constant and c.constant): + # One side is a constant: compare the other against it directly, + # turning the question round when the constant came first. + let constantRight = c.constant + let fixed = if constantRight: c else: b + let other = if constantRight: b else: c + if not constantRight: + check = swapped(check) + let bits = fixed.value + let wideBits = + if fixed.kind == FixedKind: int64(bits) + else: int64(bits) * 65536 + let sameKind = e.label() + let decided = e.label() + if other.kind == fixed.kind: + e.fastCompareConstant(other.payload, bits) + else: + let otherTag = tagOf(other) + let wide = acquire() + let bound = acquire() + if fixed.kind == WholeKind: + e.fastJumpIfZero(otherTag, sameKind) + else: + e.fastJumpIfNotZero(otherTag, sameKind) + e.fastMove(wide, other.payload) + e.fastScaleWide(wide, otherTag) + e.fastLoadWide(bound, wideBits) + e.fastCompareWide(wide, bound) + e.jump(decided) + e.place(sameKind) + e.fastCompareConstant(other.payload, bits) + e.place(decided) + elif b.kind != UnknownKind and b.kind == c.kind: + e.fastCompare(b.payload, c.payload) + else: + # Kinds that differ compare on the widened fixed-point scale. + let leftTag = tagOf(b) + let rightTag = tagOf(c) + let wideLeft = acquire() + let wideRight = acquire() + let mixed = e.label() + let decided = e.label() + e.fastJumpIfDiffer(leftTag, rightTag, mixed) + e.fastCompare(b.payload, c.payload) + e.jump(decided) + e.place(mixed) + e.fastMove(wideLeft, b.payload) + e.fastScaleWide(wideLeft, leftTag) + e.fastMove(wideRight, c.payload) + e.fastScaleWide(wideRight, rightTag) + e.fastCompareWide(wideLeft, wideRight) + e.place(decided) + let result = acquire() + e.fastAnswer(result, check) + bindValue(slot(item.a), result, -1, WholeKind) + of JumpOp: + flushAll() + e.jump(blocks[int(item.a)]) + of JumpIfZeroOp: + makeRoom([slot(item.a)], 0) + let value = fetch(slot(item.a), offset) + flushAll() + e.fastJumpIfZero(value.payload, blocks[int(item.b)]) + of JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp: + makeRoom([global(item.a)], 3) + let value = fetch(global(item.a), offset) + flushAll() + if value.kind == WholeKind: + e.fastCompareConstant(value.payload, item.b) + else: + # A fixed-point global meets the constant on the widened scale. + let tag = tagOf(value) + let wide = acquire() + let bound = acquire() + let whole = e.label() + let decided = e.label() + e.fastJumpIfZero(tag, whole) + e.fastMove(wide, value.payload) + e.fastScaleWide(wide, tag) + e.fastLoadWide(bound, int64(item.b) * 65536) + e.fastCompareWide(wide, bound) + e.jump(decided) + e.place(whole) + e.fastCompareConstant(value.payload, item.b) + e.place(decided) + e.jumpOn(takenOn(item.op), blocks[int(item.c)]) + of ArrayGetOp: + makeRoom([slot(item.c)], 2) + let index = fetch(slot(item.c), offset) + let exit = leaveHere(offset) + requireWhole(index, slot(item.c), exit) + e.fastCellAddress(index.payload, extents[int(item.b)], exit) + let payload = acquire() + let tag = acquire() + e.fastLoad(payload, tag, cell(), exit) + bindValue(slot(item.a), payload, tag, UnknownKind) + of ArraySetOp: + makeRoom([slot(item.b), slot(item.c)], 0) + let index = fetch(slot(item.b), offset) + let value = fetch(slot(item.c), offset) + let exit = leaveHere(offset) + requireWhole(index, slot(item.b), exit) + e.fastCellAddress(index.payload, extents[int(item.a)], exit) + e.fastStore(cell(), value.payload, value.tag, kindTag(value.kind)) + else: + done = false + for register in 0 ..< Pool.len: + pinned[register] = false + done + + template blockEnd(start: int): int = + ## Returns the offset just past the block starting here. + var stop = start + 1 + while stop < code.len and code[stop].op != MeterOp: + inc stop + stop + + template chainable(target: int): bool = + ## Reports whether a block can be carried on into with values still + ## held: an ordinary block, starting at its meter. + target >= 0 and target < code.len and code[target].op == MeterOp and + not inLoop[target] + + template emitFastBlock(start: int) = + ## Emits one block's fast version, then its ways out. + ## + ## Where the block carries on into another, by falling through or by + ## an unconditional jump, a copy of that one follows with every value + ## still held, so nothing is written back or read again at the seam. + ## Only this way in reaches the copy; the block's own fast version + ## serves every other. A chain stops after a few blocks, at a block + ## it has already taken in, and wherever general code took over. + held.setLen(0) + for register in 0 ..< Pool.len: + uses[register] = 0 + pinned[register] = false + var current = start + var chained = @[start] + while true: + let stop = blockEnd(current) + var handled = true + var next = -1 + for at in current ..< stop: + let item = code[at] + if at == stop - 1 and item.op == JumpOp and + chainable(int(item.a)) and int(item.a) notin chained and + chained.len < MaxChain: + next = int(item.a) + break + if emitFast(at): + handled = true + else: + handled = false + clearAll() + emitInstruction(at, false, Loop(), @[], noExits) + let last = code[stop - 1] + if next < 0 and handled and fallsOnward(last.op) and + chainable(stop) and stop notin chained and + chained.len < MaxChain: + next = stop + if next < 0: + if fallsOnward(last.op): + flushAll() + e.jump(blocks[stop]) + break + chained.add(next) + current = next + for (exit, pending, offset) in fastExits: + e.place(exit) + for value in pending: + writeBack(value) + e.jump(general[offset]) + fastExits.setLen(0) + if stubs.len > 0: + for stub in stubs: + e.place(stub.label) + e.callSlow(stub.offset, slowLabel) + case stub.carry + of ToNext: + e.jump(blocks[int(stub.offset) + 1]) + of ToOffset: + e.jump(dispatchLabel) + stubs.setLen(0) + + var index = 0 + while index < code.len: + var stop = index + 1 + while stop < code.len and code[stop].op != MeterOp: + inc stop + if code[index].op == MeterOp and not inLoop[index]: + e.place(blocks[index]) + emitFastBlock(index) + e.place(general[index]) + for at in index ..< stop: + if at > index: + e.place(blocks[at]) + emitInstruction(at, false, Loop(), @[], noExits) + else: + for at in index ..< stop: + e.place(blocks[at]) + let number = loopAt[at] + if number >= 0: + # Entering a loop's head: prove its globals whole numbers and + # move them into registers, or run it in general code if any + # is not. Every tag is looked at before any register is + # filled, because one of those registers may be what general + # code reads through. + let loop = loops[number] + e.beginHoisting() + for which in loop.globals: + e.guardHoisted(which, general[loop.start]) + for position, which in loop.globals: + e.loadHoisted(position, which) + e.jump(loopCopies[number][0]) + e.place(general[at]) + emitInstruction(at, false, Loop(), @[], noExits) + index = stop + + # Each loop's own copy, after all the general code. + for number, loop in loops: + var exits: seq[(Label, int, bool)] + let inside = loopCopies[number] + for index in loop.start ..< loop.stop: + e.place(inside[index - loop.start]) + emitInstruction(index, true, loop, inside, exits) + # Leaving writes every register back, then carries on in general + # code: at a branch target, or at the instruction that could not be + # done here, which general code then does the ordinary way. + for (stub, target, again) in exits: + e.place(stub) + e.beginHoisting() + for position, which in loop.globals: + e.storeHoisted(position, which) + e.endHoisting() + if again: + e.jump(general[target]) + else: + e.jump(blocks[target]) + + # One past the end holds nothing to run. The interpreter's code is + # left to refuse it the way it would. + e.place(blocks[code.len]) + e.callSlow(int32(code.len), slowLabel) + e.jump(dispatchLabel) + + e.place(dispatchLabel) + e.dispatch() + e.place(slowLabel) + e.slowRoutine(failedLabel, ContextStep) + e.place(hostLabel) + e.slowRoutine(failedLabel, ContextHostStep) + # The interpreter's code answers with the status to leave with: a + # failure it raised, or the compiled program retired part way. + e.place(failedLabel) + e.leaveWithAnswer() + + let bytes = e.finish() + var starts = newSeq[int](code.len + 1) + for index in 0 .. code.len: + starts[index] = e.offsetBytes(blocks[index]) + (bytes, starts) + +proc compileProgram*(code: seq[Instruction], routines: seq[RoutineExtent], + extents: seq[ArrayExtent], constants: seq[int32], globals, hostData, + arguments: int, limits: CallLimits, strings = false): Machine + {.raises: [BasicError].} = + ## Compiles every offset of a program to machine code, or returns nil + ## when this target has no backend or the program is outside what the + ## generator is sure of. Generated code indexes storage without + ## checking, so every index it will use is proved in range here first. + when not (NativeArm64 or NativeAmd64): + return nil + else: + if not layoutMatches() or code.len == 0 or routines.len == 0: + return nil + # Writing code pages from a thread that is running compiled code would + # open the write gate beneath live frames, so such a compile is + # refused and the program runs on the interpreter instead. + if runningCompiledCode(): + return nil + if limits.frames <= 0 or limits.slots < 0: + return nil + # Every value is reached through a displacement worked out here, so + # each store of values must end where one of thirty-two bits still + # reaches, whichever architecture this is. + const Reachable = (int(high(int32)) - ValueStride) div ValueStride + if globals > Reachable or hostData > Reachable or + arguments > Reachable or int(limits.slots) > Reachable: + return nil + + # Every offset belongs to exactly one routine, and none runs on into + # the next, so the routine an instruction runs in is known here. + var ownerOf = newSeq[int32](code.len) + for index in 0 ..< ownerOf.len: + ownerOf[index] = -1 + for id, routine in routines: + if routine.entry < 0 or routine.length <= 0 or + int(routine.entry) + int(routine.length) > code.len: + return nil + if routine.registers < 0 or int(routine.registers) > Reachable or + routine.parameters < 0 or + routine.parameters > routine.registers or + routine.parameters > int32(arguments): + return nil + for step in 0 ..< int(routine.length): + let offset = int(routine.entry) + step + if ownerOf[offset] >= 0: + return nil + ownerOf[offset] = int32(id) + let last = code[int(routine.entry) + int(routine.length) - 1] + if last.op notin Terminators: + return nil + for index in 0 ..< code.len: + if ownerOf[index] < 0: + return nil + + for index, item in code: + let owner = routines[int(ownerOf[index])] + template requireSlot(value: int32) = + if value < 0 or value >= owner.registers: + return nil + template requireGlobal(value: int32) = + if value < 0 or int(value) >= globals: + return nil + template requireArray(value: int32) = + if value < 0 or int(value) >= extents.len: + return nil + let extent = extents[int(value)] + if extent.base < 0 or extent.length < 0 or + int(extent.base) + int(extent.length) > Reachable: + return nil + template requireHost(value: int32) = + if value < 0 or int(value) >= hostData: + return nil + template requireArgument(value: int32) = + if value < 0 or int(value) >= arguments: + return nil + template requireTarget(value: int32) = + if value < 0 or int(value) >= code.len or + ownerOf[int(value)] != ownerOf[index]: + return nil + case item.op + of MeterOp: + if item.a < 0 or item.b < 0: + return nil + of TextCallOp: + requireSlot(item.a) + if item.c < 1 or int(item.c) > arguments: + return nil + of LoadImmediateOp: + requireSlot(item.a) + of LoadFixedOp: + requireSlot(item.a) + if item.b < 0 or int(item.b) >= constants.len: + return nil + of MoveOp, NegateOp, NotOp: + requireSlot(item.a) + requireSlot(item.b) + of LoadGlobalOp: + requireSlot(item.a) + requireGlobal(item.b) + of LoadHostDataOp: + requireSlot(item.a) + requireHost(item.b) + of StoreGlobalOp: + requireGlobal(item.a) + requireSlot(item.b) + of StoreGlobalImmediateOp, AddGlobalImmediateOp: + requireGlobal(item.a) + of MoveGlobalOp, AddGlobalOp, ModuloGlobalImmediateOp: + requireGlobal(item.a) + requireGlobal(item.b) + of AddGlobalHostDataOp: + requireGlobal(item.a) + requireHost(item.b) + of AddGlobalRegisterOp: + requireGlobal(item.a) + requireSlot(item.b) + of AddGlobalArrayGlobalIndexOp: + requireGlobal(item.a) + requireArray(item.b) + requireGlobal(item.c) + of ArrayAddGlobalsOp: + requireArray(item.a) + requireGlobal(item.b) + requireGlobal(item.c) + of AddOp, SubtractOp, MultiplyOp, DivideOp, IntegerDivideOp, + ModuloOp, EqualOp, NotEqualOp, LessOp, LessEqualOp, GreaterOp, + GreaterEqualOp, AndOp, OrOp, XorOp, EqvOp, ImpOp: + requireSlot(item.a) + requireSlot(item.b) + requireSlot(item.c) + of JumpOp, GosubOp, ReturnLabelOp: + requireTarget(item.a) + of JumpIfZeroOp: + requireSlot(item.a) + requireTarget(item.b) + of JumpUnlessGlobalEqualImmediateOp, + JumpUnlessGlobalNotEqualImmediateOp, + JumpUnlessGlobalLessImmediateOp, + JumpUnlessGlobalLessEqualImmediateOp, + JumpUnlessGlobalGreaterImmediateOp, + JumpUnlessGlobalGreaterEqualImmediateOp, + JumpUnlessGlobalModuloEqualZeroOp: + requireGlobal(item.a) + requireTarget(item.c) + of ArrayGetOp: + requireSlot(item.a) + requireArray(item.b) + requireSlot(item.c) + of ArraySetOp: + requireArray(item.a) + requireSlot(item.b) + requireSlot(item.c) + of SetArgumentOp: + requireArgument(item.a) + requireSlot(item.b) + of SetArgumentImmediateOp: + requireArgument(item.a) + of SetArgumentGlobalOp: + requireArgument(item.a) + requireGlobal(item.b) + of CallOp: + if item.a <= 0 or int(item.a) >= routines.len: + return nil + else: + discard + if item.op in {CallOp, GosubOp} and index + 1 >= code.len: + return nil + + var emitted: (seq[byte], seq[int]) + try: + emitted = emitProgram(code, routines, ownerOf, extents, constants, + limits, false, strings) + except BasicError: + # Some branch could not reach; every branch then goes the long way. + # Should that fail too, the program is left to the interpreter. + try: + emitted = emitProgram(code, routines, ownerOf, extents, constants, + limits, true, strings) + except BasicError: + return nil + let (bytes, starts) = emitted + if bytes.len > MaxProgramBytes: + return nil + + result = Machine(size: bytes.len, listing: bytes) + result.buffer = initCodeBuffer(bytes.len) + result.buffer.write(bytes) + result.buffer.seal() + result.call = cast[NativeCall](result.buffer.entry) + let origin = cast[int](result.buffer.entry) + result.table = newSeq[pointer](code.len + 1) + for index in 0 .. code.len: + result.table[index] = cast[pointer](origin + starts[index]) + +proc tableAddress*(machine: Machine): pointer {.raises: [].} = + ## Returns the table of native addresses indexed by bytecode offset. + machine.table[0].addr diff --git a/src/bassy/machine.nim b/src/bassy/machine.nim new file mode 100644 index 0000000..82e8da1 --- /dev/null +++ b/src/bassy/machine.nim @@ -0,0 +1,303 @@ +## Allocates writable-then-executable pages for generated machine code. +## Each platform enforces write-xor-execute differently, so the buffer +## keeps the page writable while emitting and seals it before any call. + +import numbers + +type + CodeBuffer* = object + ## One page-aligned region holding finished machine code. + memory: pointer + capacity: int + length: int + sealed: bool + +const + PageBytes* = 4096 + + ## Apple silicon is the one target that keeps pages executable while + ## they are written, because it gates the writing per thread instead. + AppleSilicon* = defined(macosx) and defined(arm64) + + ## Whether this target can emit and run machine code at all. Everywhere + ## else, including WebAssembly, the interpreter is the only path and no + ## platform-specific declaration is emitted. Define bassyNoJit to force + ## the interpreter on a target that would otherwise qualify. + NativeCode* = + when defined(bassyNoJit): + false + elif defined(arm64) and ( + defined(macosx) or defined(linux) or defined(windows) + ): + true + elif defined(amd64) and ( + defined(macosx) or defined(linux) or defined(windows) + ): + true + else: + false + +when NativeCode and defined(windows): + # These follow the Windows header types exactly: DWORD is an unsigned + # long there, which is a different type from an unsigned int even where + # the two are the same width. + const + MemCommit = 0x1000.culong + MemReserve = 0x2000.culong + MemRelease = 0x8000.culong + PageReadWrite = 0x04.culong + PageExecuteRead = 0x20.culong + + proc virtualAlloc(address: pointer, size: csize_t, + allocation, protection: culong): pointer + {.importc: "VirtualAlloc", header: "", stdcall.} + + proc virtualProtect(address: pointer, size: csize_t, protection: culong, + previous: ptr culong): cint + {.importc: "VirtualProtect", header: "", stdcall.} + + proc virtualFree(address: pointer, size: csize_t, freeType: culong): cint + {.importc: "VirtualFree", header: "", stdcall.} + + proc currentProcess(): pointer + {.importc: "GetCurrentProcess", header: "", stdcall.} + + proc flushInstructionCache(process, address: pointer, size: csize_t): cint + {.importc: "FlushInstructionCache", header: "", stdcall.} +elif NativeCode: + const + ProtNone = 0x0.cint + ProtRead = 0x1.cint + ProtWrite = 0x2.cint + ProtExec = 0x4.cint + MapPrivate = 0x0002.cint + MapFailed = -1 + + when AppleSilicon: + const + MapAnonymous = 0x1000.cint + MapJit = 0x0800.cint + elif defined(macosx): + const + MapAnonymous = 0x1000.cint + MapJit = 0.cint + else: + const + MapAnonymous = 0x20.cint + MapJit = 0.cint + + proc mmap(address: pointer, length: csize_t, protection, flags, + handle: cint, offset: int): pointer + {.importc: "mmap", header: "".} + + proc mprotect(address: pointer, length: csize_t, protection: cint): cint + {.importc: "mprotect", header: "".} + + proc munmap(address: pointer, length: csize_t): cint + {.importc: "munmap", header: "".} + +when NativeCode and AppleSilicon: + proc jitWriteProtect(enabled: cint) + {.importc: "pthread_jit_write_protect_np", header: "".} + + proc invalidateInstructionCache(address: pointer, length: csize_t) + {.importc: "sys_icache_invalidate", + header: "".} +elif NativeCode and defined(arm64): + proc clearCache(start, stop: pointer) + {.importc: "__builtin___clear_cache", nodecl.} + +proc `=copy`*(destination: var CodeBuffer, source: CodeBuffer) {.error: + "a code buffer owns its pages and cannot be copied".} + +proc `=destroy`*(buffer: CodeBuffer) {.raises: [].} = + ## Returns the pages when the last owner goes away, so a program that + ## compiles many scripts does not accumulate executable mappings. The + ## buffer cannot be copied, so there is exactly one owner to go away. + if buffer.memory == nil: + return + when NativeCode and defined(windows): + discard virtualFree(buffer.memory, 0.csize_t, MemRelease) + elif NativeCode: + discard munmap(buffer.memory, csize_t(buffer.capacity)) + +proc fail(message: string) {.noreturn, raises: [BasicError].} = + ## Reports a controlled code buffer failure. + raise newException(BasicError, "BASIC " & message) + +proc jitSupported*(): bool {.inline, raises: [].} = + ## Reports whether this build can emit and run native code. + NativeCode + +proc roundedToPage(size: int): int {.raises: [].} = + ## Rounds a byte count up to whole pages. + ((size + PageBytes - 1) div PageBytes) * PageBytes + +proc initCodeBuffer*(capacity: int): CodeBuffer {.raises: [BasicError].} = + ## Reserves writable pages sized to hold the requested byte count. + if capacity <= 0: + fail("code buffer capacity must be positive") + let size = roundedToPage(capacity) + when not NativeCode: + fail("this build has no native code backend") + elif defined(windows): + let memory = virtualAlloc( + nil, csize_t(size), MemCommit or MemReserve, PageReadWrite + ) + if memory == nil: + fail("code buffer reservation failed") + result = CodeBuffer( + memory: memory, capacity: size, length: 0, sealed: false + ) + else: + const OpenProtection = + when AppleSilicon: ProtRead or ProtWrite or ProtExec + else: ProtRead or ProtWrite + let memory = mmap( + nil, + csize_t(size), + OpenProtection, + MapPrivate or MapAnonymous or MapJit, + -1, + 0 + ) + if cast[int](memory) == MapFailed: + fail("code buffer reservation failed") + result = CodeBuffer( + memory: memory, capacity: size, length: 0, sealed: false + ) + +proc len*(buffer: CodeBuffer): int {.inline, raises: [].} = + ## Returns how many bytes have been emitted so far. + buffer.length + +proc capacity*(buffer: CodeBuffer): int {.inline, raises: [].} = + ## Returns the reserved byte count, rounded up to whole pages. + buffer.capacity + +var + ## How deep this thread is in writes to code pages. On Apple Silicon the + ## gate is per thread and covers every such page at once, so only the + ## outermost write may open it and only the outermost may close it. + writeDepth {.threadvar.}: int + ## How deep this thread is in running compiled code. Pages must not be + ## opened for writing while compiled frames are live beneath. + runDepth {.threadvar.}: int + ## + ## Both are per thread, as the gate is. Another thread compiling opens + ## only its own gate, and may write any such page while it is open; the + ## thread running compiled code keeps its gate closed throughout, so a + ## script cannot reach that window. Compiling from several threads at + ## once is host code the host is trusted with. + +proc beginWrite(buffer: var CodeBuffer) {.raises: [].} = + ## Makes the pages writable on platforms that enforce write-xor-execute. + when NativeCode and AppleSilicon: + if writeDepth == 0: + jitWriteProtect(0) + inc writeDepth + +proc endWrite(buffer: var CodeBuffer) {.raises: [].} = + ## Restores execute permission after a batch of writes. + dec writeDepth + when NativeCode and AppleSilicon: + if writeDepth == 0: + jitWriteProtect(1) + +proc enterCompiledCode*() {.raises: [].} = + ## Records that this thread is about to run compiled code. + inc runDepth + +proc leaveCompiledCode*() {.raises: [].} = + ## Records that this thread has come back out of compiled code. + dec runDepth + +proc runningCompiledCode*(): bool {.raises: [].} = + ## Reports whether compiled frames are live on this thread, in which case + ## no code page may be written from it. + runDepth > 0 + +proc write*(buffer: var CodeBuffer, source: pointer, size: int) + {.raises: [BasicError].} = + ## Appends raw bytes, refusing to run past the reserved pages. + if buffer.sealed: + fail("code buffer is already sealed") + if size < 0 or buffer.length + size > buffer.capacity: + fail("code buffer capacity exceeded") + if size == 0: + return + if runDepth > 0: + fail("code pages cannot be written while compiled code runs") + buffer.beginWrite() + try: + copyMem( + cast[pointer](cast[int](buffer.memory) + buffer.length), source, size + ) + finally: + buffer.endWrite() + buffer.length += size + +proc write*(buffer: var CodeBuffer, words: openArray[uint32]) + {.raises: [BasicError].} = + ## Appends fixed-width instruction words, as used by AArch64. + if words.len == 0: + return + buffer.write(words[0].addr, words.len * sizeof(uint32)) + +proc write*(buffer: var CodeBuffer, bytes: openArray[byte]) + {.raises: [BasicError].} = + ## Appends a variable-length instruction stream, as used by x86-64. + if bytes.len == 0: + return + buffer.write(bytes[0].addr, bytes.len) + +proc seal*(buffer: var CodeBuffer) {.raises: [BasicError].} = + ## Publishes the emitted bytes so the processor may execute them. + if buffer.sealed: + return + if buffer.length == 0: + fail("code buffer holds no instructions") + when not NativeCode: + fail("this build has no native code backend") + elif defined(windows): + var previous = 0.culong + if virtualProtect( + buffer.memory, csize_t(buffer.capacity), PageExecuteRead, + previous.addr + ) == 0: + fail("code buffer could not be made executable") + discard flushInstructionCache( + currentProcess(), buffer.memory, csize_t(buffer.length) + ) + buffer.sealed = true + elif AppleSilicon: + # These pages are executable already, and writing to them is what is + # gated, so there is nothing to drop here and only the cache to flush. + invalidateInstructionCache(buffer.memory, csize_t(buffer.length)) + buffer.sealed = true + else: + # Drop write as execute is granted, so the pages are never both. + if mprotect( + buffer.memory, csize_t(buffer.capacity), ProtRead or ProtExec + ) != 0: + fail("code buffer could not be made executable") + when defined(arm64): + clearCache( + buffer.memory, + cast[pointer](cast[int](buffer.memory) + buffer.length) + ) + buffer.sealed = true + +proc entry*(buffer: CodeBuffer): pointer {.raises: [BasicError].} = + ## Returns the address of the first instruction once sealed. + if not buffer.sealed: + fail("code buffer must be sealed before it is called") + buffer.memory + +proc release*(buffer: var CodeBuffer) {.raises: [].} = + ## Returns the pages early, before the owner itself goes away. + `=destroy`(buffer) + buffer.memory = nil + buffer.capacity = 0 + buffer.length = 0 + buffer.sealed = false diff --git a/src/bassy/texts.nim b/src/bassy/texts.nim index c6f7681..287ba96 100644 --- a/src/bassy/texts.nim +++ b/src/bassy/texts.nim @@ -333,3 +333,37 @@ proc reset*(storage: var TextStorage, roots: var seq[Value]) = swap(storage.arena, storage.scratch) swap(storage.spans, storage.scratchSpans) storage.owner = owner + +## Direct access for compiled code + +proc ownerAddress*(storage: var TextStorage): pointer {.raises: [].} = + ## Returns where the current generation's owner is kept. + storage.owner.addr + +proc spansAddress*(storage: var TextStorage): pointer {.raises: [].} = + ## Returns the span table itself, so its length and buffer are read + ## fresh each time, surviving the swap a reset makes. + storage.spans.addr + +proc arenaAddress*(storage: var TextStorage): pointer {.raises: [].} = + ## Returns the arena itself, read fresh each time for the same reason. + storage.arena.addr + +proc textLayoutMatches*(): bool {.raises: [].} = + ## Confirms the layout compiled code reads strings through: a span is a + ## start then a length, both thirty-two bits, and a sequence or string + ## is a length then a pointer to a capacity followed by the items. + var spans = newSeqOfCap[TextSpan](2) + spans.add TextSpan(start: 3, length: 5) + var arena = newStringOfCap(4) + arena.add "ab" + let spanOrigin = cast[int](spans[0].addr) + if sizeof(TextSpan) != 8 or + cast[int](spans[0].length.addr) - spanOrigin != 4: + return false + let spanFields = cast[ptr array[2, int]](spans.addr) + if spanFields[0] != spans.len or spanFields[1] + 8 != spanOrigin: + return false + let arenaFields = cast[ptr array[2, int]](arena.addr) + arenaFields[0] == arena.len and + arenaFields[1] + 8 == cast[int](arena[0].addr) diff --git a/tests/bench_jit.nim b/tests/bench_jit.nim new file mode 100644 index 0000000..aa09d08 --- /dev/null +++ b/tests/bench_jit.nim @@ -0,0 +1,114 @@ +## Reports how much faster compiled programs run than the interpreter. +## Times its own runs so it can execute on any CI machine without pulling +## in a benchmarking dependency. + +import + std/[monotimes, strformat, times], + bassy + +const + Runs = 5 + + # Pinned so every architecture must agree, not just the two paths. + ArithmeticResult = 1814293664'i32 + BranchResult = 977000000'i32 + NestedResult = 31962000'i32 + + ArithmeticSource = """ +i = 0 +total = 0 +while i < 1000000 + total = total + i + i = i + 1 +wend +""" + + BranchSource = """ +i = 0 +even = 0 +odd = 0 +while i < 1000000 + if i mod 2 = 0 then + even = even + 1 + else + odd = odd + 1 + end if + i = i + 1 +wend +""" + + NestedSource = """ +outer = 0 +hits = 0 +inner = 0 +while outer < 1000 + inner = 0 + while inner < 1000 + hits = hits + 1 + inner = inner + 1 + wend + outer = outer + 1 +wend +""" + +proc benchLimits(): Limits = + ## Returns limits large enough for every benchmark workload. + result = defaultLimits() + result.maxInstructions = 100_000_000 + result.maxWorkUnits = 100_000_000 + +proc fastest(runtime: var Runtime): float = + ## Returns the shortest of several runs, in milliseconds. + result = Inf + for run in 1 .. Runs: + runtime.restart() + let started = getMonoTime() + discard runtime.run() + let elapsed = (getMonoTime() - started).inNanoseconds.float / 1_000_000.0 + if elapsed < result: + result = elapsed + +proc checksum(runtime: Runtime, count: int): int32 = + ## Folds every global into one value, for comparing whole runs. + for index in 0 ..< count: + result = result *% 31'i32 +% runtime.globalValue(int32(index)).asInt + +proc measure(name, source: string, expected: int32) = + ## Times one script on both paths and reports the ratio. + ## The expected value is pinned so CI proves that generated machine code + ## produces the same answers on every architecture, not merely the same + ## answers as the interpreter running beside it. + let limits = benchLimits() + let program = compile(source, limits) + + var plain = initRuntime(program, limits) + var fast = initRuntime(program, limits) + let compiled = fast.compileNative() + + let plainTime = plain.fastest() + let fastTime = fast.fastest() + + let plainSum = plain.checksum(program.globals) + let fastSum = fast.checksum(program.globals) + let agree = plainSum == fastSum + let charged = plain.instructionsUsed == fast.instructionsUsed + + let ratio = + if fastTime > 0.0 and compiled > 0: &"{plainTime / fastTime:6.1f}x" + else: " --" + echo &" {name:<12} interpreted {plainTime:8.3f} ms " & + &"native {fastTime:8.3f} ms {ratio} " & + &"offsets {compiled} results {agree} budget {charged}" + if not agree: + quit(&"{name}: interpreted {plainSum} but native {fastSum}") + if not charged: + quit(&"{name}: budgets differ") + if plainSum != expected: + quit(&"{name}: expected {expected} but both paths gave {plainSum}") + +echo &"native compilation available: {jitSupported()}" +echo &"host: {hostCPU} {hostOS}" +measure("arithmetic", ArithmeticSource, ArithmeticResult) +measure("branches", BranchSource, BranchResult) +measure("nested", NestedSource, NestedResult) +echo "every result matched the value pinned for all architectures" diff --git a/tests/bench_raytracer.nim b/tests/bench_raytracer.nim new file mode 100644 index 0000000..48aee24 --- /dev/null +++ b/tests/bench_raytracer.nim @@ -0,0 +1,170 @@ +## Runs the BASIC raytracer on both execution paths. +## +## This is the realistic end of the workload range. Almost all of its time +## goes to fixed-point arithmetic, arrays, subroutine calls and host +## functions, and there is no single hot loop to speak of. It shows what +## compiling the whole program buys on real work, and checks that a script +## this large still agrees on both paths. + +import + std/[monotimes, strformat, times], + bassy + +const + Source = staticRead("raytracer.bas") + Size = 48 + Runs = 3 + +var pixels: seq[byte] + +proc squareRoot(arguments: openArray[Value]): Value = + ## Returns the fixed-point square root of a non-negative number. + ## + ## A Q16.16 number holds its value times 65536, so the root of those raw + ## bits shifted up by another 16 is exactly the root's own raw bits. The + ## digit-by-digit method below finds it with shifts and subtractions + ## alone, which is both faster and more predictable than iterating. + let value = arguments[0].asFixed + if value <= 0'fx: + return toValue(0'fx) + var + remainder = int64(int32(value)) shl 16 + root = 0'i64 + bit = 1'i64 shl 46 + while bit > remainder: + bit = bit shr 2 + while bit != 0: + if remainder >= root + bit: + remainder -= root + bit + root = (root shr 1) + bit + else: + root = root shr 1 + bit = bit shr 2 + toValue(Fixed(int32(root))) + +proc powerOf(arguments: openArray[Value]): Value = + ## Raises a fixed-point base to a whole exponent by repeated squaring. + ## The reference raytracer uses roughness values in the hundreds, which + ## a multiply loop in BASIC could not afford. + var + base = arguments[0].asFixed + exponent = arguments[1].asInt + total = 1'fx + if exponent <= 0: + return toValue(total) + while exponent > 0: + if (exponent and 1) != 0: + total = total * base + exponent = exponent shr 1 + if exponent == 0: + break + base = base * base + if base == 0'fx: + # The base has fallen below what Q16.16 can hold, so has the result. + return toValue(0'fx) + toValue(total) + +proc floorOf(arguments: openArray[Value]): Value = + ## Returns the largest whole number not greater than the argument. + ## Fixxy's own conversions are used because a plain int32 conversion + ## would read the raw Q16.16 bits rather than the whole part. + toValue(arguments[0].asFixed.floor.toInt) + +proc clampByte(arguments: openArray[Value]): Value = + ## Converts a colour channel to the usual zero to 255 range. + let value = arguments[0].asFixed + if value <= 0'fx: + return toValue(0'i32) + if value >= 1'fx: + return toValue(255'i32) + toValue((value * 255'fx).toInt) + +proc plot(arguments: openArray[Value]): Value = + ## Collects one rendered pixel so the image can be written out. + for index in 0 .. 2: + pixels.add(byte(arguments[index].asInt and 0xFF)) + toValue(0'i32) + +proc buildHost(): Host = + ## Supplies the numeric helpers BASIC does not provide itself. + result = initHost() + discard result.addFunction("sqr", 1, squareRoot, workUnits = 8) + discard result.addFunction("powerOf", 2, powerOf, workUnits = 8) + discard result.addFunction("floorOf", 1, floorOf, workUnits = 2) + discard result.addFunction("clampByte", 1, clampByte, workUnits = 2) + discard result.addFunction("plot", 3, plot, workUnits = 2) + discard result.addData("size", toValue(0'i32)) + discard result.addData("half", toValue(0'fx)) + discard result.addData("span", toValue(0'fx)) + +proc traceLimits(): Limits = + ## Returns limits large enough to finish the image. + result = defaultLimits() + result.maxInstructions = 2_000_000_000 + result.maxWorkUnits = 2_000_000_000 + result.maxCallDepth = 64 + +proc renderOnce(runtime: var Runtime): (float, int32) = + ## Renders the image once and returns the elapsed milliseconds. + runtime.restart() + runtime.setData("size", toValue(Size)) + runtime.setData("half", toValue(fixed(Size) / 2'fx)) + runtime.setData("span", toValue(fixed(Size) * 2'fx)) + pixels.setLen(0) + let started = getMonoTime() + discard runtime.run() + let elapsed = (getMonoTime() - started).inNanoseconds.float / 1_000_000.0 + (elapsed, runtime.getGlobal("checksum")) + +proc render(runtime: var Runtime): (float, int32) = + ## Returns the fastest of several renders and the checksum. + result[0] = Inf + for attempt in 1 .. Runs: + let (elapsed, checksum) = runtime.renderOnce() + if elapsed < result[0]: + result[0] = elapsed + result[1] = checksum + +var host = buildHost() +let limits = traceLimits() +let program = compile(Source, host, limits) + +echo &"native compilation available: {jitSupported()}" +echo &"host: {hostCPU} {hostOS}" +echo &"image: {Size} by {Size}, bytecode {program.instructions} instructions" + +var plain = initRuntime(program, host, limits) +var fast = initRuntime(program, host, limits) +let compiled = fast.compileNative() + +let (plainTime, plainSum) = plain.render() +let (fastTime, fastSum) = fast.render() + +echo &" interpreted {plainTime:9.2f} ms checksum {plainSum}" +echo &" native {fastTime:9.2f} ms checksum {fastSum} " & + &"compiled offsets {compiled}" +if compiled > 0 and fastTime > 0.0: + echo &" ratio {plainTime / fastTime:9.2f}x" +echo &" instructions charged: {plain.instructionsUsed} vs " & + &"{fast.instructionsUsed}" + +# Fixed point is the reason this VM has no floats. The same scene must +# therefore render to the same bytes on every architecture, so the +# checksum is pinned rather than merely compared between the two paths. +const ExpectedChecksum = 1356659'i32 +if plainSum != ExpectedChecksum: + quit(&"expected checksum {ExpectedChecksum} but rendered {plainSum}") + +if plainSum != fastSum: + quit("the two paths disagreed on the image") +if plain.instructionsUsed != fast.instructionsUsed: + quit("the two paths disagreed on the budget") + +if pixels.len == Size * Size * 3: + var text = &"P6\n{Size} {Size}\n255\n" + for value in pixels: + text.add(char(value)) + writeFile("tests/raytracer.ppm", text) + echo " wrote tests/raytracer.ppm" + +echo "raytracer agrees on both paths" diff --git a/tests/bench_strings.nim b/tests/bench_strings.nim new file mode 100644 index 0000000..9417ec6 --- /dev/null +++ b/tests/bench_strings.nim @@ -0,0 +1,86 @@ +## Runs the BASIC text workload on both execution paths. +## +## The string counterpart of the raytracer: splitting, slicing, reversing, +## encoding, comparing and scoring words, where nearly every instruction +## touches a string. It shows what compiling buys when most of the work is +## string storage rather than arithmetic, and checks that a script this +## string-heavy still agrees on both paths. + +import + std/[monotimes, strformat, times], + bassy + +const + Source = staticRead("strings.bas") + Rounds = 200 + Runs = 5 + +proc textLimits(): Limits = + ## Returns limits large enough for every string the workload builds. + result = defaultLimits() + result.maxInstructions = 2_000_000_000 + result.maxWorkUnits = 2_000_000_000 + result.maxStrings = 200_000 + result.maxStringBytes = 4 * 1024 * 1024 + result.maxMemoryBytes = 256 * 1024 * 1024 + +proc processOnce(runtime: var Runtime): (float, int32) = + ## Runs the workload once from a fresh state, timing it. + runtime.reset() + runtime.setGlobal("rounds", toValue(Rounds.int32)) + let started = getMonoTime() + discard runtime.run() + let elapsed = (getMonoTime() - started).inNanoseconds.float / 1_000_000.0 + (elapsed, runtime.getGlobal("checksum")) + +proc process(runtime: var Runtime): (float, int32) = + ## Returns the fastest of several runs and the checksum. + result[0] = Inf + for attempt in 1 .. Runs: + let (elapsed, checksum) = runtime.processOnce() + if elapsed < result[0]: + result[0] = elapsed + result[1] = checksum + +let limits = textLimits() +let program = compile(Source, limits) + +echo &"native compilation available: {jitSupported()}" +echo &"host: {hostCPU} {hostOS}" +echo &"rounds: {Rounds}, bytecode {program.instructions} instructions" + +var plain = initRuntime(program, limits) +var fast = initRuntime(program, limits) +let compiled = fast.compileNative() + +let (plainTime, plainSum) = plain.process() +let (fastTime, fastSum) = fast.process() + +echo &" interpreted {plainTime:9.2f} ms checksum {plainSum}" +echo &" native {fastTime:9.2f} ms checksum {fastSum} " & + &"compiled offsets {compiled}" +if compiled > 0 and fastTime > 0.0: + echo &" ratio {plainTime / fastTime:9.2f}x" +echo &" instructions charged: {plain.instructionsUsed} vs " & + &"{fast.instructionsUsed}" +echo &" work charged: {plain.workUsed} vs {fast.workUsed}" +echo &" strings: {plain.stringCount} vs {fast.stringCount}, " & + &"bytes {plain.stringBytes} vs {fast.stringBytes}" + +# The checksum is pinned, so every architecture must build the very same +# strings, not merely agree with the interpreter running beside it. +const ExpectedChecksum = 350997'i32 +if plainSum != ExpectedChecksum: + quit(&"expected checksum {ExpectedChecksum} but computed {plainSum}") + +if plainSum != fastSum: + quit("the two paths disagreed on the checksum") +if plain.instructionsUsed != fast.instructionsUsed: + quit("the two paths disagreed on the instruction budget") +if plain.workUsed != fast.workUsed: + quit("the two paths disagreed on the work budget") +if plain.stringCount != fast.stringCount or + plain.stringBytes != fast.stringBytes: + quit("the two paths disagreed on the strings they built") + +echo "the text workload agrees on both paths" diff --git a/tests/raytracer.bas b/tests/raytracer.bas new file mode 100644 index 0000000..3300d12 --- /dev/null +++ b/tests/raytracer.bas @@ -0,0 +1,293 @@ +' A port of the vmath raytracer benchmark, itself based on +' https://github.com/edin/raytracer, to Bassy's BASIC. +' +' Three differences follow from the language rather than the algorithm. +' Numbers are Q16.16 fixed point, so "far away" is 20000 rather than a +' million and the light falloff is coarser. There are no records, so the +' scene lives in parallel arrays. Subroutines cannot return values and +' every scalar that is not a parameter is global, so anything that must +' survive a recursive call is passed as an argument. + +dim thingKind(3) +dim thingSurface(3) +dim thingAx(3) +dim thingAy(3) +dim thingAz(3) +dim thingExtra(3) + +dim lightX(4) +dim lightY(4) +dim lightZ(4) +dim lightR(4) +dim lightG(4) +dim lightB(4) + +' Surfaces +shiny = 0 +checkerboard = 1 + +' Shapes +planeKind = 0 +sphereKind = 1 + +farAway = 200 +maxDepth = 5 +thingCount = 3 +lightCount = 4 + +sub setPlane(slot, nx, ny, nz, offset, surface) + thingKind(slot) = planeKind + thingSurface(slot) = surface + thingAx(slot) = nx + thingAy(slot) = ny + thingAz(slot) = nz + thingExtra(slot) = offset +end sub + +sub setSphere(slot, cx, cy, cz, radius, surface) + thingKind(slot) = sphereKind + thingSurface(slot) = surface + thingAx(slot) = cx + thingAy(slot) = cy + thingAz(slot) = cz + thingExtra(slot) = radius * radius +end sub + +sub setLight(slot, lx, ly, lz, r, g, b) + lightX(slot) = lx + lightY(slot) = ly + lightZ(slot) = lz + lightR(slot) = r + lightG(slot) = g + lightB(slot) = b +end sub + +' Writes a unit vector into normX, normY, normZ. +sub normalize(vx, vy, vz) + mag = sqr(vx * vx + vy * vy + vz * vz) + normMag = mag + if mag = 0 then + normX = 0 + normY = 0 + normZ = 0 + exit sub + end if + normX = vx / mag + normY = vy / mag + normZ = vz / mag +end sub + +' Writes the surface normal at a point into normX, normY, normZ. +sub surfaceNormal(thing, px, py, pz) + if thingKind(thing) = sphereKind then + normalize(px - thingAx(thing), py - thingAy(thing), pz - thingAz(thing)) + else + normX = thingAx(thing) + normY = thingAy(thing) + normZ = thingAz(thing) + end if +end sub + +' Writes the nearest hit into hitThing and hitDist. A hitThing of -1 means +' the ray escaped. +sub intersections(sx, sy, sz, dx, dy, dz) + hitThing = -1 + hitDist = farAway + if dy < 0 then + floorDist = sy / (0 - dy) + if floorDist < hitDist then + hitThing = 0 + hitDist = floorDist + end if + end if + probe = 1 + while probe < thingCount + eox = thingAx(probe) - sx + eoy = thingAy(probe) - sy + eoz = thingAz(probe) - sz + v = eox * dx + eoy * dy + eoz * dz + if v >= 0 then + disc = thingExtra(probe) - (eox * eox + eoy * eoy + eoz * eoz - v * v) + if disc >= 0 then + candidateDist = v - sqr(disc) + if candidateDist <> 0 then + if candidateDist < hitDist then + hitThing = probe + hitDist = candidateDist + end if + end if + end if + end if + probe = probe + 1 + wend +end sub + +' Writes the surface description at a point into diffuseR/G/B, +' specularR/G/B, reflectance and roughness. +sub surfaceAt(thing, px, py, pz) + if thingSurface(thing) = shiny then + diffuseR = 1 + diffuseG = 1 + diffuseB = 1 + specularR = 0.5 + specularG = 0.5 + specularB = 0.5 + reflectance = 0.7 + roughness = 250 + else + squares = floorOf(pz) + floorOf(px) + if squares mod 2 <> 0 then + reflectance = 0.1 + diffuseR = 1 + diffuseG = 1 + diffuseB = 1 + else + reflectance = 0.7 + diffuseR = 0 + diffuseG = 0 + diffuseB = 0 + end if + specularR = 1 + specularG = 1 + specularB = 1 + roughness = 150 + end if +end sub + +' Adds one light's contribution to lightSumR/G/B. +sub applyLight(slot) + ldx = lightX(slot) - px + ldy = lightY(slot) - py + ldz = lightZ(slot) - pz + normalize(ldx, ldy, ldz) + illum = normX * nx + normY * ny + normZ * nz + specular = normX * rdx + normY * rdy + normZ * rdz + if illum <= 0 then + if specular <= 0 then + exit sub + end if + end if + lightRange = normMag + intersections(px + normX * 0.002, py + normY * 0.002, pz + normZ * 0.002, normX, normY, normZ) + if hitThing >= 0 then + if hitDist <= lightRange then + exit sub + end if + end if + if illum > 0 then + lightSumR = lightSumR + illum * lightR(slot) * diffuseR + lightSumG = lightSumG + illum * lightG(slot) * diffuseG + lightSumB = lightSumB + illum * lightB(slot) * diffuseB + end if + if specular > 0 then + falloff = powerOf(specular, roughness) + lightSumR = lightSumR + falloff * lightR(slot) * specularR + lightSumG = lightSumG + falloff * lightG(slot) * specularR + lightSumB = lightSumB + falloff * lightB(slot) * specularR + end if +end sub + +sub traceRay(sx, sy, sz, dx, dy, dz, depth) + intersections(sx, sy, sz, dx, dy, dz) + if hitThing < 0 then + colorR = 0 + colorG = 0 + colorB = 0 + exit sub + end if + shade(hitThing, hitDist, sx, sy, sz, dx, dy, dz, depth) +end sub + +' Combines the already-computed natural colour with a reflected ray. Every +' value it needs is a parameter, because the recursive call below will +' overwrite the globals the caller was using. +sub addReflection(naturalR, naturalG, naturalB, weight, px, py, pz, rdx, rdy, rdz, depth) + traceRay(px + rdx * 0.002, py + rdy * 0.002, pz + rdz * 0.002, rdx, rdy, rdz, depth + 1) + colorR = naturalR + colorR * weight + colorG = naturalG + colorG * weight + colorB = naturalB + colorB * weight +end sub + +sub shade(thing, dist, sx, sy, sz, dx, dy, dz, depth) + px = sx + dx * dist + py = sy + dy * dist + pz = sz + dz * dist + surfaceNormal(thing, px, py, pz) + nx = normX + ny = normY + nz = normZ + reflectDot = nx * dx + ny * dy + nz * dz + rdx = dx - 2 * reflectDot * nx + rdy = dy - 2 * reflectDot * ny + rdz = dz - 2 * reflectDot * nz + surfaceAt(thing, px, py, pz) + + lightSumR = 0 + lightSumG = 0 + lightSumB = 0 + slot = 0 + while slot < lightCount + applyLight(slot) + slot = slot + 1 + wend + + if depth >= maxDepth then + colorR = lightSumR + 0.5 + colorG = lightSumG + 0.5 + colorB = lightSumB + 0.5 + exit sub + end if + addReflection(lightSumR, lightSumG, lightSumB, reflectance, px, py, pz, rdx, rdy, rdz, depth) +end sub + +' Scene +setPlane(0, 0, 1, 0, 0, checkerboard) +setSphere(1, 0, 1, -0.25, 1, shiny) +setSphere(2, -1, 0.5, 1.5, 0.5, shiny) +setLight(0, -2, 2.5, 0, 0.49, 0.07, 0.07) +setLight(1, 1.5, 2.5, 1.5, 0.07, 0.07, 0.49) +setLight(2, 1.5, 2.5, -1.5, 0.07, 0.49, 0.071) +setLight(3, 0, 3.5, 0, 0.21, 0.21, 0.35) + +' Camera at (3, 2, 4) looking at (-1, 0.5, 0) +normalize(-4, -1.5, -4) +forwardX = normX +forwardY = normY +forwardZ = normZ +' right = forward cross down, with down = (0, -1, 0) +rightRawX = forwardY * 0 - forwardZ * -1 +rightRawY = forwardZ * 0 - forwardX * 0 +rightRawZ = forwardX * -1 - forwardY * 0 +normalize(rightRawX, rightRawY, rightRawZ) +rightX = normX * 1.5 +rightY = normY * 1.5 +rightZ = normZ * 1.5 +upRawX = forwardY * rightRawZ - forwardZ * rightRawY +upRawY = forwardZ * rightRawX - forwardX * rightRawZ +upRawZ = forwardX * rightRawY - forwardY * rightRawX +normalize(upRawX, upRawY, upRawZ) +upX = normX * 1.5 +upY = normY * 1.5 +upZ = normZ * 1.5 + +checksum = 0 +py2 = 0 +while py2 < size + px2 = 0 + while px2 < size + recenterX = (px2 - half) / span + recenterY = (half - py2) / span + rayX = forwardX + recenterX * rightX + recenterY * upX + rayY = forwardY + recenterX * rightY + recenterY * upY + rayZ = forwardZ + recenterX * rightZ + recenterY * upZ + normalize(rayX, rayY, rayZ) + traceRay(3, 2, 4, normX, normY, normZ, 0) + red = clampByte(colorR) + green = clampByte(colorG) + blue = clampByte(colorB) + checksum = checksum + red + green * 3 + blue * 7 + plot(red, green, blue) + px2 = px2 + 1 + wend + py2 = py2 + 1 +wend diff --git a/tests/strings.bas b/tests/strings.bas new file mode 100644 index 0000000..f0e7728 --- /dev/null +++ b/tests/strings.bas @@ -0,0 +1,79 @@ +rem A text workload: split sentences into words, then reverse, encode, +rem compare and score every word. It leans on every string function the +rem language has, the way a chatty game script would. + +dim words$(15) +checksum = 0 +palindromes = 0 +repeats = 0 +shouts = 0 +round = 0 +while round < rounds + sentence = 0 + while sentence < 4 + select case (sentence + round) mod 4 + case 0 + text$ = "the quick brown fox jumps over the lazy dog" + case 1 + text$ = "a man a plan a canal panama" + case 2 + text$ = "never odd or even said the racecar driver" + case else + text$ = " pack my box with five dozen liquor jugs " + end select + text$ = trim$(text$) + + rem Split on spaces. + count = 0 + start = 1 + finish = instr(start, text$, " ") + while finish > 0 + words$(count) = mid$(text$, start, finish - start) + count = count + 1 + start = finish + 1 + finish = instr(start, text$, " ") + wend + words$(count) = mid$(text$, start) + count = count + 1 + + index = 0 + while index < count + word$ = words$(index) + size = len(word$) + + rem Reverse it one letter at a time. + reversed$ = "" + letter = size + while letter > 0 + reversed$ = reversed$ + mid$(word$, letter, 1) + letter = letter - 1 + wend + if reversed$ = word$ and size > 1 then palindromes = palindromes + 1 + + rem Shift every letter three places along the alphabet. + coded$ = "" + letter = 1 + while letter <= size + code = asc(mid$(word$, letter, 1)) - 97 + coded$ = coded$ + chr$((code + 3) mod 26 + 97) + letter = letter + 1 + wend + + rem Count words seen earlier in the same sentence. + earlier = 0 + while earlier < index + if words$(earlier) = word$ then repeats = repeats + 1 + earlier = earlier + 1 + wend + + if ucase$(word$) = "THE" then shouts = shouts + 1 + checksum = (checksum * 31 + asc(coded$) * size + len(reversed$)) mod 1000003 + index = index + 1 + wend + checksum = (checksum + len(left$(text$, 5)) + len(right$(text$, 3))) mod 1000003 + sentence = sentence + 1 + wend + round = round + 1 +wend +summary$ = str$(checksum) + str$(palindromes) + str$(repeats) + str$(shouts) +checksum = (checksum + len(summary$)) mod 1000003 diff --git a/tests/test_amd64.nim b/tests/test_amd64.nim new file mode 100644 index 0000000..1681f04 --- /dev/null +++ b/tests/test_amd64.nim @@ -0,0 +1,230 @@ +## Checks every x86-64 encoder against the system assembler. +## Each case names the instruction in Intel syntax and emits it with the +## encoder, then both byte streams must agree exactly. + +import + std/[os, osproc, sequtils, strutils], + bassy/amd64 + +var cases: seq[(string, seq[byte])] + +template encodes(text: string, body: untyped) = + ## Records one assembly line beside the bytes the encoder produced. + block: + var assembler {.inject.} = Assembler() + body + assembler.resolve() + cases.add((text, assembler.code)) + +proc disassemble(body: string, name: string): seq[string] = + ## Assembles a fragment and returns its disassembled instruction text. + ## Both the encoder's bytes and the reference line go through this same + ## path, so a shorter but equivalent encoding still compares equal. + let + directory = getTempDir() / "bassy-amd64-check" + source = directory / name & ".s" + objectFile = directory / name & ".o" + createDir(directory) + writeFile(source, ".intel_syntax noprefix\n" & body) + let build = execCmdEx( + "clang -c -target x86_64-apple-macos -o " & objectFile & " " & source + ) + if build.exitCode != 0: + quit("assembler rejected " & name & ":\n" & build.output & body) + let dump = execCmdEx("otool -tV -X -arch x86_64 " & objectFile) + if dump.exitCode != 0: + quit("otool failed:\n" & dump.output) + for line in dump.output.splitLines: + let fields = line.splitWhitespace() + if fields.len < 2 or not fields[0].endsWith(":"): + continue + result.add(fields[1 .. ^1].join(" ")) + +proc byteLines(bytes: seq[byte]): string = + ## Renders encoder output as assembler byte directives. + for value in bytes: + result.add("\t.byte 0x" & value.toHex(2) & "\n") + +if findExe("otool") == "" or findExe("clang") == "": + echo "skipping: needs clang and otool for the reference encoding" + quit(0) + +## Moves + +encodes "mov ebx, esi": + assembler.moveRegister(Word32, rbx, rsi) +encodes "mov rbx, r14": + assembler.moveRegister(Word64, rbx, r14) +encodes "mov r15, rdi": + assembler.moveRegister(Word64, r15, rdi) +encodes "mov ecx, 4660": + assembler.loadImmediate(Word32, rcx, 4660) +encodes "mov r9d, -1": + assembler.loadImmediate(Word32, r9, -1) +encodes "movabs r10, 4294967296": + assembler.loadImmediate(Word64, r10, 4294967296'i64) + +## Memory + +encodes "mov esi, dword ptr [rbx + 24]": + assembler.loadWord(rsi, rbx, 24) +encodes "mov dword ptr [rbx + 4104], r14d": + assembler.storeWord(r14, rbx, 4104) +encodes "mov rbx, qword ptr [rdi]": + assembler.loadDouble(rbx, rdi, 0) +encodes "mov r12, qword ptr [rdi + 8]": + assembler.loadDouble(r12, rdi, 8) +encodes "mov qword ptr [rdi + 16], r13": + assembler.storeDouble(r13, rdi, 16) +encodes "movzx eax, byte ptr [rbx + 32]": + assembler.loadByteZeroed(rax, rbx, 32) +encodes "movzx r11d, byte ptr [rbx]": + assembler.loadByteZeroed(r11, rbx, 0) +encodes "mov byte ptr [rbx + 48], 0": + assembler.storeByteImmediate(rbx, 48, 0) + +encodes "mov byte ptr [rbp + 16], r11b": + assembler.storeByteLow(rbp, 16, r11) +encodes "mov byte ptr [rbx], al": + assembler.storeByteLow(rbx, 0, rax) + +## Arithmetic + +encodes "add esi, ecx": + assembler.addRegister(Word32, rsi, rcx) +encodes "add r14d, r15d": + assembler.addRegister(Word32, r14, r15) +encodes "sub r12, rax": + assembler.subtractRegister(Word64, r12, rax) +encodes "add ecx, 1": + assembler.addImmediate(Word32, rcx, 1) +encodes "add r8d, -3": + assembler.addImmediate(Word32, r8, -3) +encodes "sub esi, 100": + assembler.subtractImmediate(Word32, rsi, 100) +encodes "cmp r14d, 1000000": + assembler.compareImmediate(Word32, r14, 1000000) +encodes "cmp esi, ecx": + assembler.compareRegister(Word32, rsi, rcx) +encodes "cmp r12, rax": + assembler.compareRegister(Word64, r12, rax) +encodes "test edx, edx": + assembler.testRegister(Word32, rdx, rdx) +encodes "imul ecx, esi": + assembler.multiplyRegister(Word32, rcx, rsi) +encodes "cdq": + assembler.signExtendToPair(Word32) +encodes "cqo": + assembler.signExtendToPair(Word64) +encodes "idiv r11d": + assembler.signedDivide(Word32, r11) +encodes "idiv ecx": + assembler.signedDivide(Word32, rcx) + +encodes "neg eax": + assembler.negateRegister(Word32, rax) +encodes "neg r11d": + assembler.negateRegister(Word32, r11) +encodes "setl al\n\tmovzx eax, al": + assembler.setIfCondition(rax, LessCondition) +encodes "sete dl\n\tmovzx edx, dl": + assembler.setIfCondition(rdx, EqualCondition) +encodes "setne sil\n\tmovzx esi, sil": + assembler.setIfCondition(rsi, NotEqualCondition) +encodes "setg dil\n\tmovzx edi, dil": + assembler.setIfCondition(rdi, GreaterCondition) +encodes "setle r9b\n\tmovzx r9d, r9b": + assembler.setIfCondition(r9, LessEqualCondition) + +encodes "shl rax, 4": + assembler.shiftLeftImmediate(Word64, rax, 4) +encodes "shl r11d, 1": + assembler.shiftLeftImmediate(Word32, r11, 1) + +encodes "movsxd rax, ecx": + assembler.signExtendDouble(rax, rcx) +encodes "movsxd r11, edx": + assembler.signExtendDouble(r11, rdx) +encodes "sar rax, 16": + assembler.shiftRightImmediate(Word64, rax, 16) + +## Stack and control flow + +encodes "push rbx": + assembler.push(rbx) +encodes "push r15": + assembler.push(r15) +encodes "pop r12": + assembler.pop(r12) +encodes "pop rbp": + assembler.pop(rbp) +encodes "and eax, ecx": + assembler.andRegister(Word32, rax, rcx) +encodes "and r9d, esi": + assembler.andRegister(Word32, r9, rsi) +encodes "or edi, r10d": + assembler.orRegister(Word32, rdi, r10) +encodes "xor r8d, r9d": + assembler.xorRegister(Word32, r8, r9) +encodes "xor rax, rdx": + assembler.xorRegister(Word64, rax, rdx) +encodes "not ecx": + assembler.notRegister(Word32, rcx) +encodes "not r10d": + assembler.notRegister(Word32, r10) +encodes "call rax": + assembler.callRegister(rax) +encodes "call r11": + assembler.callRegister(r11) +encodes "jmp rcx": + assembler.jumpRegister(rcx) +encodes "jmp r11": + assembler.jumpRegister(r11) +block: + var assembler = Assembler() + let target = assembler.label() + assembler.callLabel(target) + assembler.place(target) + assembler.returnToCaller() + assembler.resolve() + cases.add(("call 1f\n1:\tret", assembler.code)) +encodes "test eax, 7": + assembler.testImmediate(Word32, rax, 7) +encodes "test r14d, 1023": + assembler.testImmediate(Word32, r14, 1023) +encodes "ret": + assembler.returnToCaller() + +## Branch displacement resolution + +block: + var assembler = Assembler() + let top = assembler.label() + let done = assembler.label() + assembler.place(top) + assembler.compareImmediate(Word32, rsi, 10) + assembler.branchIf(GreaterEqualCondition, done) + assembler.addRegister(Word32, rcx, rsi) + assembler.branch(top) + assembler.place(done) + assembler.returnToCaller() + assembler.resolve() + cases.add(( + "1:\tcmp esi, 10\n\tjge 2f\n\tadd ecx, esi\n\tjmp 1b\n2:\tret", + assembler.code + )) + +var failures = 0 +for index, (expected, produced) in cases: + let wanted = disassemble("\t" & expected & "\n", "wanted" & $index) + let got = disassemble(produced.byteLines, "got" & $index) + if wanted != got: + inc failures + echo "mismatch for: ", expected.replace("\n\t", " ; ") + echo " encoder bytes: ", produced.mapIt(it.toHex(2)).join(" ") + echo " encoder means: ", got.join(" ; ") + echo " reference: ", wanted.join(" ; ") + +if failures > 0: + quit($failures & " of " & $cases.len & " encodings disagree") +echo "all ", cases.len, " x86-64 encodings decode to the intended instruction" diff --git a/tests/test_arm64.nim b/tests/test_arm64.nim new file mode 100644 index 0000000..b5ac47b --- /dev/null +++ b/tests/test_arm64.nim @@ -0,0 +1,228 @@ +## Checks every AArch64 encoder against the system assembler. +## Each case names the instruction in assembly text and emits it with the +## encoder, then both byte streams must agree exactly. + +import + std/[os, osproc, sequtils, strutils], + bassy/arm64 + +var cases: seq[(string, seq[uint32])] + +template encodes(text: string, body: untyped) = + ## Records one assembly line beside the words the encoder produced. + block: + var assembler {.inject.} = Assembler() + body + assembler.resolve() + cases.add((text, assembler.code)) + +proc assembled(lines: seq[string]): seq[seq[uint32]] = + ## Assembles each line with clang and returns its instruction words. + let + directory = getTempDir() / "bassy-arm64-check" + source = directory / "check.s" + objectFile = directory / "check.o" + createDir(directory) + var text = "" + for line in lines: + text.add("\t" & line & "\n") + writeFile(source, text) + let build = execCmdEx( + "clang -c -target arm64-apple-macos -o " & objectFile & " " & source + ) + if build.exitCode != 0: + quit("assembler rejected a reference line:\n" & build.output) + let dump = execCmdEx("otool -t -X " & objectFile) + if dump.exitCode != 0: + quit("otool failed:\n" & dump.output) + var words: seq[uint32] + for line in dump.output.splitLines: + let fields = line.splitWhitespace() + if fields.len < 2: + continue + for index in 1 ..< fields.len: + words.add(uint32(parseHexInt(fields[index]))) + # Regroup the flat word stream back into per-case instruction counts. + var start = 0 + for (_, produced) in cases: + var chunk: seq[uint32] + for index in 0 ..< produced.len: + if start >= words.len: + quit("assembler produced fewer words than the encoder") + chunk.add(words[start]) + inc start + result.add(chunk) + if start != words.len: + quit("assembler produced more words than the encoder") + +if findExe("otool") == "" or findExe("clang") == "": + echo "skipping: needs clang and otool for the reference encoding" + quit(0) + +## Moves and immediates + +encodes "mov w3, w7": + assembler.moveRegister(Word32, x3, x7) +encodes "mov x3, x7": + assembler.moveRegister(Word64, x3, x7) +encodes "movz x3, #4660, lsl #16": + assembler.moveZero(Word64, x3, 4660, 16) +encodes "movk w5, #255": + assembler.moveKeep(Word32, x5, 255) +encodes "movn x9, #1": + assembler.moveNot(Word64, x9, 1) + +## Arithmetic + +encodes "add w2, w2, w1": + assembler.addRegister(Word32, x2, x2, x1) +encodes "add x10, x11, x12, lsl #4": + assembler.addRegister(Word64, x10, x11, x12, 4) +encodes "add w1, w1, #1": + assembler.addImmediate(Word32, x1, x1, 1) +encodes "sub x4, x5, #4095": + assembler.subtractImmediate(Word64, x4, x5, 4095) +encodes "sub w6, w7, w8": + assembler.subtractRegister(Word32, x6, x7, x8) +encodes "cmp w1, w0": + assembler.compareRegister(Word32, x1, x0) +encodes "cmp x1, #17": + assembler.compareImmediate(Word64, x1, 17) +encodes "neg w3, w4": + assembler.negate(Word32, x3, x4) +encodes "mul w1, w2, w3": + assembler.multiply(Word32, x1, x2, x3) +encodes "madd x1, x2, x3, x4": + assembler.multiplyAdd(Word64, x1, x2, x3, x4) +encodes "msub w9, w10, w11, w12": + assembler.multiplySubtract(Word32, x9, x10, x11, x12) +encodes "sdiv w1, w2, w3": + assembler.signedDivide(Word32, x1, x2, x3) + +encodes "smull x1, w2, w3": + assembler.signedMultiplyLong(x1, x2, x3) +encodes "asr x4, x5, #16": + assembler.arithmeticShiftRight(Word64, x4, x5, 16) +encodes "asr w6, w7, #3": + assembler.arithmeticShiftRight(Word32, x6, x7, 3) + +## Logic + +encodes "and w1, w2, w3": + assembler.andRegister(Word32, x1, x2, x3) +encodes "orr x1, x2, x3": + assembler.orRegister(Word64, x1, x2, x3) +encodes "eor w4, w5, w6": + assembler.xorRegister(Word32, x4, x5, x6) +encodes "mvn w7, w8": + assembler.notRegister(Word32, x7, x8) +encodes "csetm w1, lt": + assembler.setOnCondition(Word32, x1, LessCondition) +encodes "csetm w2, eq": + assembler.setOnCondition(Word32, x2, EqualCondition) +encodes "csetm x3, ge": + assembler.setOnCondition(Word64, x3, GreaterEqualCondition) + +encodes "tst w22, #1": + assembler.testLowBits(Word32, x22, 1) +encodes "tst w3, #7": + assembler.testLowBits(Word32, x3, 3) +encodes "tst x4, #0xffff": + assembler.testLowBits(Word64, x4, 16) + +encodes "sxtw x5, w6": + assembler.signExtendWord(x5, x6) +encodes "lsl x7, x8, #16": + assembler.shiftLeftImmediate(Word64, x7, x8, 16) +encodes "lsl w9, w10, #3": + assembler.shiftLeftImmediate(Word32, x9, x10, 3) +encodes "lsr x11, x12, #1": + assembler.shiftRightImmediate(Word64, x11, x12, 1) +encodes "lsr w13, w14, #8": + assembler.shiftRightImmediate(Word32, x13, x14, 8) + +## Memory + +encodes "ldr w5, [x6, #12]": + assembler.loadWord(x5, x6, 12) +encodes "str w7, [x8]": + assembler.storeWord(x7, x8) +encodes "ldr x9, [x10, #4088]": + assembler.loadDouble(x9, x10, 4088) +encodes "str x11, [x12, #16]": + assembler.storeDouble(x11, x12, 16) +encodes "stp x29, x30, [sp, #-32]!": + assembler.storePair(framePointer, linkRegister, stackPointer, -32, true) +encodes "ldp x29, x30, [sp], #32": + assembler.loadPair(framePointer, linkRegister, stackPointer, 32, true) +encodes "ldp x19, x20, [sp, #16]": + assembler.loadPair(x19, x20, stackPointer, 16) + +## Branches + +encodes "blr x16": + assembler.callRegister(x16) +encodes "br x9": + assembler.jumpRegister(x9) +encodes "ret": + assembler.returnToCaller() + +## Multi-word immediate construction + +encodes "movz w0, #0": + assembler.loadImmediate(Word32, x0, 0) +encodes "movz w0, #4660": + assembler.loadImmediate(Word32, x0, 4660) +encodes "movn w1, #60875": + assembler.loadImmediate(Word32, x1, 0xFFFF1234) +encodes "movn x2, #0": + assembler.loadImmediate(Word64, x2, -1) +encodes "movz x3, #22136\n\tmovk x3, #43981, lsl #32": + assembler.loadImmediate(Word64, x3, 0x0000ABCD_00005678'i64) + +## Branch displacement resolution + +block: + var assembler = Assembler() + let top = assembler.label() + let done = assembler.label() + assembler.place(top) + assembler.compareRegister(Word32, x1, x0) + assembler.branchIf(GreaterEqualCondition, done) + assembler.addRegister(Word32, x2, x2, x1) + assembler.branch(top) + assembler.place(done) + assembler.returnToCaller() + assembler.resolve() + cases.add(( + "cmp w1, w0\n\tb.ge 1f\n\tadd w2, w2, w1\n\tb . - 12\n1:\tret", + assembler.code + )) + +block: + var assembler = Assembler() + let done = assembler.label() + assembler.branchIfZero(Word32, x4, done) + assembler.branchIfNotZero(Word64, x5, done) + assembler.place(done) + assembler.returnToCaller() + assembler.resolve() + cases.add(("cbz w4, 1f\n\tcbnz x5, 1f\n1:\tret", assembler.code)) + +var lines: seq[string] +for (text, _) in cases: + lines.add(text) + +let reference = assembled(lines) +var failures = 0 +for index, (text, produced) in cases: + let expected = reference[index] + if produced != expected: + inc failures + echo "mismatch for: ", text.replace("\n\t", " ; ") + echo " encoder: ", produced.mapIt(it.toHex(8)).join(" ") + echo " assembler: ", expected.mapIt(it.toHex(8)).join(" ") + +if failures > 0: + quit($failures & " of " & $cases.len & " encodings disagree") +echo "all ", cases.len, " AArch64 encodings match the system assembler" diff --git a/tests/test_jit.nim b/tests/test_jit.nim new file mode 100644 index 0000000..c064846 --- /dev/null +++ b/tests/test_jit.nim @@ -0,0 +1,191 @@ +## Checks that compiled programs agree with the interpreter. +## Every script runs twice, once interpreted and once with the whole program +## executed as machine code. The globals and both budgets must match +## exactly, because a script must not be able to tell which path ran. + +import + std/strformat, + bassy + +type Outcome = object + globals: seq[Value] + instructions: int64 + work: int64 + failure: string + compiled: int + +proc describe(value: Value): string = + ## Renders a global for comparison output. + case value.kind + of IntegerValue: $value.asInt + of FixedValue: $value.asFixed + of StringValue: "" + of ArrayValue, BlobValue: "" + +proc execute(source: string, native: bool): Outcome = + ## Runs one script with or without native compilation. + let program = compile(source) + var runtime = initRuntime(program) + if native: + result.compiled = runtime.compileNative() + try: + discard runtime.run() + except BasicError as error: + result.failure = error.msg + for index in 0 ..< program.globals: + result.globals.add(runtime.globalValue(int32(index))) + let (instructions, work) = runtime.remainingBudget + result.instructions = instructions + result.work = work + +proc check(name, source: string) = + ## Compares the two execution paths and reports any disagreement. + let plain = execute(source, false) + let fast = execute(source, true) + var problems: seq[string] + if jitSupported() and fast.compiled == 0: + problems.add("the program was not compiled") + if plain.globals.len != fast.globals.len: + problems.add("global count differs") + else: + for index in 0 ..< plain.globals.len: + if plain.globals[index].describe != fast.globals[index].describe: + problems.add( + &"global {index}: interpreted {plain.globals[index].describe} " & + &"but native {fast.globals[index].describe}" + ) + if plain.instructions != fast.instructions: + problems.add( + &"instruction budget: interpreted {plain.instructions} " & + &"but native {fast.instructions}" + ) + if plain.work != fast.work: + problems.add( + &"work budget: interpreted {plain.work} but native {fast.work}" + ) + if plain.failure != fast.failure: + problems.add( + &"failure: interpreted '{plain.failure}' but native '{fast.failure}'" + ) + if problems.len > 0: + echo "FAIL ", name + for problem in problems: + echo " ", problem + quit(1) + let note = + if fast.compiled > 0: &"{fast.compiled} compiled" + else: "interpreted only" + echo &" ok {name:<34} {note}" + +echo "native compilation available: ", jitSupported() + +check "counting loop", """ +i = 0 +total = 0 +while i < 1000 + total = total + i + i = i + 1 +wend +""" + +check "integer wraparound", """ +i = 0 +total = 0 +while i < 100000 + total = total + 987654321 + i = i + 1 +wend +""" + +check "modulo branch", """ +i = 0 +even = 0 +while i < 1000 + if i mod 2 = 0 then + even = even + 1 + end if + i = i + 1 +wend +""" + +check "negative step", """ +i = 100 +total = 0 +while i > 0 + total = total + i + i = i + -1 +wend +""" + +check "nested loops", """ +outer = 0 +inner = 0 +hits = 0 +while outer < 50 + inner = 0 + while inner < 20 + hits = hits + 1 + inner = inner + 1 + wend + outer = outer + 1 +wend +""" + +check "loop that never runs", """ +i = 500 +total = 0 +while i < 100 + total = total + 1 + i = i + 1 +wend +""" + +# A counter that is fixed point from the start takes the other kind's +# path through every comparison and add without changing the answer. +check "a fixed-point counter", """ +i = 0.5 +total = 0 +while i < 10 + total = total + 1 + i = i + 1 +wend +""" + +# The instruction budget has to be refused at exactly the same point. +proc checkBudget(name: string, source: string, maximum: int64) = + ## Compares budget exhaustion between the two paths. + var limits = defaultLimits() + limits.maxInstructions = maximum + let program = compile(source, limits) + var plainRuntime = initRuntime(program, limits) + var plainFailure = "" + try: + discard plainRuntime.run() + except BasicError as error: + plainFailure = error.msg + var fastRuntime = initRuntime(program, limits) + discard fastRuntime.compileNative() + var fastFailure = "" + try: + discard fastRuntime.run() + except BasicError as error: + fastFailure = error.msg + let (plainLeft, _) = plainRuntime.remainingBudget + let (fastLeft, _) = fastRuntime.remainingBudget + if plainFailure != fastFailure or plainLeft != fastLeft: + echo "FAIL ", name + echo &" interpreted '{plainFailure}' left {plainLeft}" + echo &" native '{fastFailure}' left {fastLeft}" + quit(1) + echo &" ok {name:<34} stopped with {plainLeft} left" + +checkBudget("instruction budget runs out", """ +i = 0 +total = 0 +while i < 1000000 + total = total + i + i = i + 1 +wend +""", 5000) + +echo "native compilation matches the interpreter" diff --git a/tests/test_jit_safety.nim b/tests/test_jit_safety.nim new file mode 100644 index 0000000..8896282 --- /dev/null +++ b/tests/test_jit_safety.nim @@ -0,0 +1,893 @@ +## Tries to get the native compiler to do something the interpreter would +## not. Compiled code indexes global storage without checking and writes +## through offsets worked out at compile time, so the checks that make +## that safe are the ones worth attacking. +## +## Two halves. The first hands compileProgram bytecode the language's own +## compiler would never produce, and requires it to refuse rather than +## emit. The second runs generated scripts down both paths and requires +## the results and both budgets to match, because a script that could tell +## the difference could be written to exploit it. + +import + std/[importutils, random, strformat, strutils], + bassy {.all.}, + bassy/jit + +var failures = 0 + +proc report(name: string, ok: bool, detail = "") = + ## Records one check. + if ok: + echo &" ok {name}" + else: + inc failures + echo &"FAIL {name}" + if detail.len > 0: + echo &" {detail}" + +## The layout the code generator assumes + +report( + "value and context layout is the one the generator writes", + layoutMatches(), + "compiled stores would land at the wrong offsets" +) + +## Bytecode the language could not produce + +const + Globals = 4 + Slots = 8 + Limits = CallLimits(frames: 8, slots: 64) + +proc countingLoop(globalIndex: int32, target: int32): seq[Instruction] = + ## A minimal program, parameterised so it can be made malformed. + @[ + Instruction(op: MeterOp, a: 4, b: 2), + Instruction( + op: JumpUnlessGlobalLessImmediateOp, a: globalIndex, b: 10, c: target + ), + Instruction(op: AddGlobalImmediateOp, a: globalIndex, b: 1), + Instruction(op: JumpOp, a: 0), + Instruction(op: MeterOp, a: 1, b: 1), + Instruction(op: HaltOp) + ] + +proc main(code: seq[Instruction]): seq[RoutineExtent] = + ## One routine covering the whole program. + @[RoutineExtent(entry: 0, length: int32(code.len), registers: Slots)] + +proc compiles(code: seq[Instruction], globals = Globals, + routines: seq[RoutineExtent] = @[], extents: seq[ArrayExtent] = @[], + constants: seq[int32] = @[], hostData = 0, arguments = 4): bool = + ## Reports whether the whole program was accepted. + let table = if routines.len > 0: routines else: main(code) + compileProgram(code, table, extents, constants, globals, hostData, + arguments, Limits) != nil + +block: + # The same shape must compile when it is well formed, or the refusals + # below would prove nothing. + report( + "a well formed program still compiles", + (not jitSupported()) or countingLoop(1, 4).compiles + ) + +report( + "a global one past the end is refused", + not countingLoop(Globals, 4).compiles, + "an out of range index would become a fixed offset store" +) + +report( + "a global far past the end is refused", + not countingLoop(1_000_000, 4).compiles +) + +report("a negative global is refused", not countingLoop(-1, 4).compiles) + +report( + "a branch past the end of the code is refused", + not countingLoop(1, 99).compiles +) + +report( + "a negative branch target is refused", + not countingLoop(1, -5).compiles +) + +report( + "a program with no storage behind it is refused", + not countingLoop(1, 4).compiles(globals = 0) +) + +block: + # Nothing may hand the generator a global so far out that its offset + # would not fit the displacement it is reached through. + report( + "a global whose offset would not fit is refused", + not countingLoop(high(int32) div 8, 4).compiles(globals = high(int32)) + ) + +block: + var code = countingLoop(1, 4) + code[2] = Instruction(op: LoadGlobalOp, a: int32(Slots), b: 1) + report("a register slot past the frame is refused", not code.compiles) + code[2] = Instruction(op: LoadGlobalOp, a: -1, b: 1) + report("a negative register slot is refused", not code.compiles) + code[2] = Instruction(op: JumpIfZeroOp, a: int32(Slots), b: 4) + report("a jump-if-zero slot past the frame is refused", not code.compiles) + +block: + var code = countingLoop(1, 4) + code[2] = Instruction(op: ArrayGetOp, a: 0, b: 7, c: 1) + report("an array that does not exist is refused", not code.compiles) + report( + "an array with a negative length is refused", + not code.compiles(extents = @[ + ArrayExtent(base: 0, length: 1), ArrayExtent(base: 0, length: 1), + ArrayExtent(base: 0, length: 1), ArrayExtent(base: 0, length: 1), + ArrayExtent(base: 0, length: 1), ArrayExtent(base: 0, length: 1), + ArrayExtent(base: 0, length: 1), ArrayExtent(base: 0, length: -4) + ]) + ) + +block: + var code = countingLoop(1, 4) + code[2] = Instruction(op: LoadFixedOp, a: 0, b: 3) + report("a fixed-point constant that does not exist is refused", + not code.compiles(constants = @[1'i32])) + code[2] = Instruction(op: LoadHostDataOp, a: 0, b: 2) + report("host data that does not exist is refused", + not code.compiles(hostData = 2)) + code[2] = Instruction(op: SetArgumentImmediateOp, a: 4, b: 1) + report("an argument past the staging area is refused", + not code.compiles(arguments = 4)) + +block: + let code = countingLoop(1, 4) + report( + "a routine table that leaves code uncovered is refused", + not code.compiles(routines = @[ + RoutineExtent(entry: 0, length: 4, registers: Slots) + ]) + ) + report( + "routines that overlap are refused", + not code.compiles(routines = @[ + RoutineExtent(entry: 0, length: 6, registers: Slots), + RoutineExtent(entry: 4, length: 2, registers: Slots) + ]) + ) + report( + "a routine reaching past the code is refused", + not code.compiles(routines = @[ + RoutineExtent(entry: 0, length: 9, registers: Slots) + ]) + ) + report( + "a routine that runs on into the next one is refused", + not code.compiles(routines = @[ + RoutineExtent(entry: 0, length: 2, registers: Slots), + RoutineExtent(entry: 2, length: 4, registers: Slots) + ]) + ) + report( + "a branch into another routine is refused", + not countingLoop(1, 4).compiles(routines = @[ + RoutineExtent(entry: 0, length: 4, registers: Slots), + RoutineExtent(entry: 4, length: 2, registers: Slots) + ]) + ) + +block: + var code = countingLoop(1, 4) + code[2] = Instruction(op: CallOp, a: 0) + report("a call to the main program is refused", not code.compiles) + code[2] = Instruction(op: CallOp, a: 5) + report("a call to a routine that does not exist is refused", + not code.compiles) + +## Scripts, down both paths + +type Outcome = object + globals: seq[int64] + instructions: int64 + work: int64 + failure: string + +proc execute(source: string, native: bool, maximum: int64): Outcome = + ## Runs one script and records everything a script could observe. + var limits = defaultLimits() + limits.maxInstructions = maximum + limits.maxWorkUnits = maximum + let program = compile(source, limits) + var runtime = initRuntime(program, limits) + if native: + discard runtime.compileNative() + try: + discard runtime.run() + except BasicError as error: + result.failure = error.msg + for index in 0 ..< program.globals: + let value = runtime.globalValue(int32(index)) + result.globals.add( + case value.kind + of IntegerValue: int64(value.asInt) + of FixedValue: int64(int32(value.asFixed)) + of StringValue: -1'i64 + of ArrayValue, BlobValue: -2'i64 + ) + let (instructions, work) = runtime.remainingBudget + result.instructions = instructions + result.work = work + +proc agrees(name, source: string, maximum = 2_000_000'i64) = + ## Requires the two paths to be indistinguishable from inside a script. + let plain = execute(source, false, maximum) + let fast = execute(source, true, maximum) + var detail = "" + if plain.globals != fast.globals: + detail = &"globals {plain.globals} then {fast.globals}" + elif plain.instructions != fast.instructions: + detail = &"instructions {plain.instructions} then {fast.instructions}" + elif plain.work != fast.work: + detail = &"work {plain.work} then {fast.work}" + elif plain.failure != fast.failure: + detail = &"failure '{plain.failure}' then '{fast.failure}'" + report(name, detail.len == 0, detail) + +agrees("a loop wider than any budget still stops", """ +i = 0 +total = 0 +while i < 2000000000 + total = total + 1 + i = i + 1 +wend +""") + +agrees("a loop that never advances still stops", """ +i = 0 +seen = 0 +while i < 10 + seen = seen + 1 +wend +""") + +agrees("nesting cannot outrun the budget", """ +a = 0 +b = 0 +hits = 0 +while a < 100000 + b = 0 + while b < 100000 + hits = hits + 1 + b = b + 1 + wend + a = a + 1 +wend +""") + +agrees("wrapping at the top of the range", """ +i = 0 +total = 2147483647 +while i < 100 + total = total + 1 + i = i + 1 +wend +""") + +agrees("wrapping at the bottom of the range", """ +i = 0 +total = -2147483648 +while i < 100 + total = total + -1 + i = i + 1 +wend +""") + +agrees("the most negative value against minus one", """ +i = -2147483648 +hits = 0 +while i < -2147483638 + if i mod -1 = 0 then + hits = hits + 1 + end if + i = i + 1 +wend +""") + +agrees("a remainder against the most negative divisor", """ +i = 0 +hits = 0 +while i < 50 + if i mod -2147483648 = 0 then + hits = hits + 1 + end if + i = i + 1 +wend +""") + +agrees("a counter that turns fractional mid-run", """ +i = 0 +total = 0 +while i < 200 + total = total + 1 + i = i + 1 + if i = 100 then + i = i + 0.5 + end if +wend +""") + +agrees("a budget that runs out inside a loop", """ +i = 0 +total = 0 +while i < 1000000 + total = total + i + i = i + 1 +wend +""", maximum = 733) + +agrees("a budget that runs out on the first pass", """ +i = 0 +total = 0 +while i < 1000000 + total = total + i + i = i + 1 +wend +""", maximum = 3) + +agrees("arrays beside a compiled loop", """ +dim cells(15) +i = 0 +total = 0 +while i < 16 + cells(i) = i + i = i + 1 +wend +j = 0 +while j < 16 + total = total + cells(j) + j = j + 1 +wend +""") + +agrees("reading past the end of an array", """ +dim cells(15) +i = 0 +total = 0 +while i < 40 + total = total + cells(i) + i = i + 1 +wend +""") + +agrees("writing past the end of an array", """ +dim cells(15) +i = 0 +while i < 40 + cells(i) = i + i = i + 1 +wend +""") + +agrees("a negative array index", """ +dim cells(15) +i = 5 +total = 0 +while i > -5 + total = total + cells(i) + i = i + -1 +wend +""") + +agrees("an array holding fixed point", """ +dim cells(15) +i = 0 +total = 0 +while i < 16 + cells(i) = i + 0.5 + i = i + 1 +wend +j = 0 +while j < 16 + total = total + cells(j) + j = j + 1 +wend +""") + +agrees("fixed point arithmetic", """ +x = 0.5 +delta = 0.25 +total = 0.0 +i = 0 +while i < 200 + total = total + x * delta + x = x - delta + i = i + 1 +wend +""") + +agrees("fixed point comparison", """ +x = 0.0 +hits = 0 +i = 0 +while i < 300 + x = x + 0.125 + if x > 10.0 then + hits = hits + 1 + end if + i = i + 1 +wend +""") + +agrees("mixing whole and fixed operands", """ +x = 0.5 +n = 3 +total = 0 +i = 0 +while i < 100 + total = total + n + x = x + 0.25 + i = i + 1 +wend +""") + +agrees("fixed point that wraps", """ +x = 32767.0 +i = 0 +while i < 50 + x = x + 100.0 + i = i + 1 +wend +""") + +agrees("modulo and integer divide", """ +i = 0 +sum = 0 +while i < 400 + sum = sum + (i mod 7) + sum = sum + (i \ 5) + i = i + 1 +wend +""") + +agrees("a divisor that reaches zero", """ +d = 3 +i = 0 +total = 0 +while i < 10 + total = total + (100 mod d) + d = d - 1 + i = i + 1 +wend +""") + +agrees("dividing the most negative by minus one", """ +a = -2147483648 +d = -1 +i = 0 +total = 0 +while i < 5 + total = total + (a \ d) + i = i + 1 +wend +""") + +agrees("fixed point held in array cells", """ +dim cells(63) +i = 0 +while i < 64 + cells(i) = 0.5 + i = i + 1 +wend +i = 0 +while i < 63 + cells(i) = cells(i) + cells(i + 1) + i = i + 1 +wend +j = 0 +while j < 63 + cells(j) = cells(j) * cells(j + 1) + j = j + 1 +wend +""") + +agrees("a loop that calls a subroutine", """ +sub bump(v) + total = total + v +end sub +i = 0 +total = 0 +while i < 200 + bump(i) + bump(i) + i = i + 1 +wend +""") + +agrees("a subroutine calling another", """ +sub inner(v) + total = total + v +end sub +sub outer(v) + inner(v) + inner(v) +end sub +i = 0 +total = 0 +while i < 200 + outer(i) + i = i + 1 +wend +""") + +agrees("recursion", """ +sub down(n) + if n > 0 then + hits = hits + 1 + down(n - 1) + end if +end sub +i = 0 +hits = 0 +while i < 100 + down(8) + i = i + 1 +wend +""") + +agrees("recursion past the depth limit", """ +sub down(n) + hits = hits + 1 + down(n + 1) +end sub +i = 0 +hits = 0 +while i < 3 + down(1) + i = i + 1 +wend +""") + +agrees("leaving a subroutine early", """ +sub maybe(v) + if v > 50 then + exit sub + end if + total = total + v +end sub +i = 0 +total = 0 +while i < 200 + maybe(i) + i = i + 1 +wend +""") + +agrees("a callee that reaches an array", """ +dim cells(63) +sub store(n) + cells(n) = n * 2 +end sub +i = 0 +while i < 64 + store(i) + i = i + 1 +wend +""") + +agrees("a callee doing something unmodelled", """ +sub shout(v) + print v +end sub +i = 0 +while i < 20 + shout(i) + i = i + 1 +wend +""") + +agrees("the budget running out inside a call", """ +sub bump(v) + total = total + v +end sub +i = 0 +total = 0 +while i < 100000 + bump(i) + i = i + 1 +wend +""", maximum = 977) + +agrees("dividing between array cells", """ +dim cells(63) +i = 0 +while i < 64 + cells(i) = 100.0 + i = i + 1 +wend +i = 1 +while i < 64 + cells(i) = cells(i) / cells(i - 1) + cells(i) = cells(i) + 99.5 + i = i + 1 +wend +""") + +agrees("dividing negatives between cells", """ +dim cells(63) +i = 0 +while i < 64 + cells(i) = 0.0 - 7.25 + i = i + 1 +wend +i = 1 +while i < 64 + cells(i) = cells(i) / cells(i - 1) + cells(i) = cells(i) - 8.25 + i = i + 1 +wend +""") + +agrees("dividing by zero part way", """ +d = 3 +total = 0.0 +i = 0 +while i < 10 + total = total + 100 / d + d = d - 1 + i = i + 1 +wend +""") + +agrees("dividing a whole too large to widen", """ +n = 40000 +total = 0.0 +i = 0 +while i < 10 + total = total + n / 2 + i = i + 1 +wend +""") + +## Generated scripts + +proc generated(seed: int64): string = + ## Builds a small integer program out of the shapes the compiler models. + var random = initRand(seed) + let names = ["a", "b", "c", "d"] + result = "" + for name in names: + result.add(&"{name} = {random.rand(-40 .. 40)}\n") + let counter = names[random.rand(0 .. 3)] + result.add(&"{counter} = 0\n") + result.add(&"while {counter} < {random.rand(1 .. 60)}\n") + for statement in 0 ..< random.rand(1 .. 4): + let target = names[random.rand(0 .. 3)] + case random.rand(0 .. 3) + of 0: + result.add(&" {target} = {target} + {random.rand(-9 .. 9)}\n") + of 1: + result.add(&" {target} = {target} + {names[random.rand(0 .. 3)]}\n") + of 2: + let divisor = [2, 4, 8, 3, -2][random.rand(0 .. 4)] + result.add(&" if {target} mod {divisor} = 0 then\n") + result.add(&" {target} = {target} + 1\n") + result.add(" end if\n") + else: + result.add(&" {target} = {random.rand(-30 .. 30)}\n") + result.add(&" {counter} = {counter} + 1\n") + result.add("wend\n") + +var generatedFailures = 0 +for seed in 1'i64 .. 400'i64: + let source = generated(seed) + let plain = execute(source, false, 2_000_000) + let fast = execute(source, true, 2_000_000) + if plain.globals != fast.globals or + plain.instructions != fast.instructions or + plain.work != fast.work or plain.failure != fast.failure: + inc generatedFailures + if generatedFailures == 1: + echo "first disagreement, seed ", seed, ":" + echo source + echo &" interpreted {plain.globals} {plain.instructions} " & + &"{plain.work} '{plain.failure}'" + echo &" native {fast.globals} {fast.instructions} " & + &"{fast.work} '{fast.failure}'" +report( + "400 generated scripts agree on both paths", + generatedFailures == 0, + &"{generatedFailures} disagreed" +) + +## Host code that replaces the runtime's buffers + +block: + # A runtime-aware callback moves every buffer compiled code reaches + # into fresh memory, keeping the contents, then returns into compiled + # code. Everything after must land in the new buffers, not the freed. + proc relocated(native: bool): string = + var host = initHost() + let move: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + privateAccess(Runtime) + runtime.globals = runtime.globals & @[] + runtime.memory = runtime.memory & @[] + runtime.registers = runtime.registers & @[] + runtime.frames = runtime.frames & @[] + runtime.arguments = runtime.arguments & @[] + runtime.hostData = runtime.hostData & @[] + arguments[0] + discard host.addFunction("move", 1, move) + let program = compile(""" +dim cells(7) +sub stash(n) + cells(n mod 8) = cells(n mod 8) + n + total = total + move(n) +end sub +i = 0 +while i < 200 + a = a + i + stash(i) + b = b + cells(i mod 8) * 2 + i = i + 1 +wend +""", host) + var runtime = initRuntime(program, host) + if native: + discard runtime.compileNative() + discard runtime.run() + result = $runtime.getGlobal("a") & " " & $runtime.getGlobal("b") & + " " & $runtime.getGlobal("total") + for index in 0 ..< 8: + result.add(" " & $runtime.getArray("cells", int32(index))) + let plain = relocated(false) + let fast = relocated(true) + report("buffers replaced by host code are followed, not written through", + plain == fast, &"{plain} then {fast}") + +block: + # Host code that shrinks a store the compiled program indexes directly. + # The indexes were proved against the old lengths, so the compiled + # program is retired and the rest runs interpreted, where every access + # is checked. The answer must be the interpreter's own, failure and all. + # Without bounds checks the interpreter itself has no defined answer. + when compileOption("boundChecks"): + var source = "dim cells(7)\n" + for index in 0 ..< 90: + source.add(&"g{index} = {index}\n") + source.add(""" +sub bump(n) + cells(n mod 8) = cells(n mod 8) + n +end sub +i = 0 +while i < 20 + g80 = g80 + shrink(i) + cells(3) = cells(3) + g80 + bump(i) + i = i + 1 +wend +""") + for mode in ["globals", "no globals", "no cells", "registers"]: + proc shrunk(native: bool): string = + var host = initHost() + var calls = 0 + let shrink: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + privateAccess(Runtime) + inc calls + if calls == 5: + case mode + of "globals": runtime.globals = newSeq[Value](1) + of "no globals": runtime.globals.setLen(0) + of "no cells": runtime.memory.setLen(0) + else: runtime.registers.setLen(1) + toValue(1'i32) + discard host.addFunction("shrink", 1, shrink) + let program = compile(source, host) + var runtime = initRuntime(program, host) + if native: + discard runtime.compileNative() + try: + discard runtime.run() + result = "finished" + except Exception as error: + result = $error.name & ": " & error.msg + let plain = shrunk(false) + let fast = shrunk(true) + report(&"host code shrinking {mode} ends as the interpreter does", + plain == fast, &"{plain} then {fast}") + +block: + # Host code that points a live frame's return at no offset at all. The + # return must refuse before it pops anything, so the frame is still on + # and a second run refuses the same way rather than carrying on a call + # further down. + privateAccess(Runtime) + privateAccess(Frame) + var host = initHost() + let corrupt: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + runtime.frames[int(runtime.depth) - 1].returnPc = 9999 + toValue(0'i32) + discard host.addFunction("corrupt", 0, corrupt) + let program = compile("sub inner()\n a = corrupt()\nend sub\ninner()\nb = 1\n", + host) + var runtime = initRuntime(program, host) + if runtime.compileNative() > 0: + var messages: seq[string] + for attempt in 0 .. 1: + try: + discard runtime.run() + messages.add("finished") + except BasicError as error: + messages.add(error.msg) + report("a corrupted return offset is refused before the frame is popped", + messages.len == 2 and "outside the program" in messages[0] and + messages[1] == messages[0] and runtime.depth == 1, + &"{messages} depth {runtime.depth}") + +## Offsets outside the program + +block: + # No legal program reaches an offset outside itself. Should one ever be + # handed to compiled code anyway, it must end in an ordinary refusal, + # not a jump through memory past the table or a defect. + privateAccess(Runtime) + let program = compile("a = 1\nb = a + 1\n") + for offset in [int32(program.instructions), int32(program.instructions + 7), + -1'i32, high(int32)]: + if not jitSupported(): + # With no compiled code there is nothing here to test, and the + # interpreter is not what this refusal is asked of. + break + var runtime = initRuntime(program) + discard runtime.compileNative() + runtime.pc = offset + var refused = "" + try: + discard runtime.run() + except BasicError as error: + refused = error.msg + report(&"compiled code refuses offset {offset}", + (not jitSupported()) or "outside the program" in refused, refused) + +## Compiling from inside compiled code + +block: + # A host callback that compiles and runs another program while this one + # is running as machine code must not open code pages for writing + # beneath live frames. That compile is refused, the inner program runs + # on the interpreter, and compiling works again once the outer returns. + let inner = compile("x = 0\nwhile x < 50\n x = x + 2\nwend\n") + var innerCompiled = -1 + var innerResult = 0'i32 + var host = initHost() + let nest: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + var other = initRuntime(inner) + innerCompiled = other.compileNative() + discard other.run() + innerResult = other.getGlobal("x") + toValue(innerResult) + discard host.addFunction("nest", 0, nest) + let program = compile("a = nest()\nb = a + 1\n", host) + var runtime = initRuntime(program, host) + discard runtime.compileNative() + discard runtime.run() + report("compiling beneath compiled frames is refused", + (not jitSupported()) or innerCompiled == 0, $innerCompiled) + report("the refused program still runs, interpreted", + innerResult == 50 and runtime.getGlobal("b") == 51, + &"{innerResult} {runtime.getGlobal(\"b\")}") + var after = initRuntime(inner) + report("compiling works again once compiled code returns", + (not jitSupported()) or after.compileNative() == inner.instructions) + +if failures > 0: + quit($failures & " safety checks failed") +echo "native compilation is indistinguishable from interpretation" diff --git a/tests/test_native.nim b/tests/test_native.nim new file mode 100644 index 0000000..2e61549 --- /dev/null +++ b/tests/test_native.nim @@ -0,0 +1,711 @@ +## Checks that a whole program compiled to machine code is +## indistinguishable from the same program interpreted. +## +## Generated programs mix whole numbers, fixed point and strings, arrays, +## subs, GOSUB, host functions, printing, and every way a script can fail: +## bad indices, zero divisors, wrong kinds, exhausted budgets, deep calls, +## and host code that raises. Each runs down both paths and everything a +## host could observe afterwards is compared: every global and array cell, +## every print event, both budgets, the offset it stopped at, the string +## storage, and the exact failure, including its exception type. + +import + std/[random, strformat, strutils, tables], + bassy + +const + Scalars = ["a", "b", "c", "d"] + Decimals = ["x", "y"] + Texts = ["s$", "t$"] + Cells = 6 + TextCells = 3 + TableCells = 6 + +var + failures = 0 + compiled = 0 + handed = 0'i64 + executed = 0'i64 + +proc render(runtime: Runtime, value: Value): string = + ## Renders one value with its kind, and a string by its content. + case value.kind + of IntegerValue: "i" & $value.asInt + of FixedValue: "f" & $int32(value.asFixed) + of StringValue: + try: + "s" & runtime.getString(value).escape + except CatchableError: + "s" + of ArrayValue, BlobValue: + # Each runtime has its own buffer generation, so only the slot is + # something the two paths must agree on. + "b" & $value.kind & $value.bufferSlot + +proc makeHost(): Host = + ## Builds host data and functions of every kind a script can call. + result = initHost() + discard result.addData("seed", toValue(7'i32)) + discard result.addData("scale", toValue(fixed(3'i32) / fixed(2'i32))) + discard result.addData("mail$", "hello") + discard result.addFunction("twice", 1, + proc(arguments: openArray[int32]): int32 = + if arguments[0] == 13: + raise newException(ValueError, "host refuses thirteen") + arguments[0] *% 2 + ) + discard result.addFunction("halve", 1, + proc(arguments: openArray[Value]): Value = + if arguments[0].kind == FixedValue: + toValue(arguments[0].asFixed / fixed(2'i32)) + else: + toValue(arguments[0].asInt div 2) + ) + discard result.addFunction("pick", 2, + proc(arguments: openArray[int32]): int32 = + if arguments[0] > arguments[1]: arguments[0] else: arguments[1] + , 3) + # Runtime-aware callbacks: one reads an array through a checked view, + # charging per cell, and writes the total into the first cell of + # another; the other burns work. Either can refuse part way. + let gather: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + let source = runtime.arrayView(arguments[0]) + let target = runtime.arrayView(arguments[1], writable = true) + runtime.chargeOperations(int64(source.len)) + var total = toValue(0'i32) + for index in 0 ..< source.len: + total = total + source[index] + target[0] = total + toValue(int32(source.len)) + discard result.addFunction("gather", 2, gather) + let burn: ContextHostProc = proc(runtime: Runtime, + arguments: openArray[Value]): Value = + runtime.chargeWork(int64(arguments[0].asInt)) + arguments[0] + discard result.addFunction("burn", 1, burn) + +proc observe(source: string, native: bool, limits: Limits, + runs: int): string = + ## Runs a program and writes down everything a host could observe. + var transcript: seq[string] + let host = makeHost() + let program = compile(source, host, limits) + var runtime = initRuntime(program, host, limits) + if native: + let count = runtime.compileNative() + if jitSupported(): + doAssert count == program.instructions, + "the whole program should compile" + inc compiled + for run in 0 ..< runs: + # The second run carries on from wherever the first stopped, which + # after a failure is part way through a block; the third restarts. + if run == 2: + runtime.restart + var events: seq[string] + let print = proc(event: PrintEvent) = + events.add(&"{event.kind}:{event.text.escape}:{event.value}:" & + &"{int32(event.fixedValue)}") + try: + let stats = runtime.run(print) + transcript.add(&"stats {stats.instructions} {stats.workUnits} " & + &"{stats.printBytes} {stats.printEvents}") + except CatchableError as error: + transcript.add(&"raised {error.name}: {error.msg}") + transcript.add("printed " & events.join(" ")) + if native: + executed += runtime.instructionsUsed + let (instructions, work) = runtime.remainingBudget + transcript.add(&"budget {instructions} {work} at {runtime.offset}") + transcript.add(&"strings {runtime.stringCount} {runtime.stringBytes}") + for index in 0 ..< program.globals: + transcript.add(&"global {index} " & + runtime.render(runtime.globalValue(int32(index)))) + for index in 0 ..< Cells: + transcript.add(&"cell {index} " & + runtime.render(runtime.getArrayValue("cells", int32(index)))) + for index in 0 ..< TextCells: + transcript.add(&"text {index} " & + runtime.render(runtime.getArrayValue("words$", int32(index)))) + for index in 0 ..< TableCells: + transcript.add(&"table {index} " & + runtime.render(runtime.getArrayValue("table", int32(index)))) + if native: + handed += runtime.handedBack + transcript.join("\n") + +proc agree(name, source: string, limits = defaultLimits(), runs = 3) = + ## Requires both paths to leave exactly the same trail. + let plain = observe(source, false, limits, runs) + let fast = observe(source, true, limits, runs) + if plain != fast: + inc failures + echo &"FAIL {name}" + echo source + let plainLines = plain.splitLines + let fastLines = fast.splitLines + for index in 0 ..< max(plainLines.len, fastLines.len): + let left = if index < plainLines.len: plainLines[index] else: "" + let right = if index < fastLines.len: fastLines[index] else: "" + if left != right: + echo &" interpreted {left}" + echo &" native {right}" + else: + echo &" ok {name}" + +## Programs worth naming + +const Preamble = """ +dim cells(5) +dim words$(2) +data table = 3, -1, 0.5, 2147483647, -7.25, 13 +data weights as fixed32 = 0.5, 1.25, -3 +""" + +agree("arithmetic of every kind", Preamble & """ +a = 7 +b = -3 +x = 1.5 +y = -0.25 +c = a * b + a \ b - a mod b +d = x * y + x / y - x +e = -x +f = a / b +g = (a < b) + (x >= y) * 2 + (a = 7) +h = not a and 12 or 3 xor 5 +i = a eqv b +j = a imp b +k = x + 2 +l = 2 * y +""") + +agree("strings through every function", Preamble & """ +s$ = "Hello, World" +t$ = s$ + " again" +a = len(t$) +words$(0) = left$(s$, 5) +words$(1) = right$(s$, 5) +words$(2) = mid$(s$, 3, 4) +s$ = ucase$(s$) + lcase$(t$) + trim$(" x ") + ltrim$(" y") + rtrim$("z ") +b = asc("A") +t$ = chr$(66) + space$(2) + string$(3, "q") + str$(42) + str$(-1.5) +c = instr(s$, "WORLD") +d = s$ < t$ +e = s$ = s$ +print s$; t$ +print a, b, c +""") + +agree("subs, recursion, and GOSUB", Preamble & """ +sub fact(n) + if n <= 1 then + a = a + 1 + exit sub + end if + b = b + n + fact(n - 1) +end sub +sub shared(n) + gosub bump + gosub bump + c = c + n + exit sub +bump: + n = n + 1 + return +end sub +fact(10) +shared(5) +gosub outer +d = 99 +end +outer: + cells(1) = cells(1) + 1 + return +""") + +agree("an array index out of range", Preamble & """ +a = 3 +cells(a) = 4 +a = a + 3 +cells(a) = 5 +""") + +agree("a fixed-point index names a whole cell", Preamble & """ +x = 2.0 +cells(x) = 9 +y = 2.5 +cells(y) = 1 +""") + +agree("dividing by zero", Preamble & """ +a = 5 +b = 0 +c = a \ b +""") + +agree("fixed-point division by zero", Preamble & """ +x = 5.5 +y = 0.0 +c = x / y +""") + +agree("a whole number too wide for fixed point", Preamble & """ +a = 40000 +x = 0.5 +y = a + x +""") + +agree("adding a string to a number", Preamble & """ +s$ = "a" +a = len(s$) +t$ = s$ + str$(a) +b = a + len(t$) +""") + +agree("host code that raises", Preamble & """ +a = twice(6) +b = halve(a) +x = halve(3.0) +c = pick(a, b) +d = twice(13) +e = 1 +""") + +agree("host data", Preamble & """ +a = seed + seed +x = scale * 2 +s$ = mail$ + "!" +b = a + seed +""") + +agree("printing", Preamble & """ +print "a"; 1; -2 +print 1.5, -0.125 +s$ = "text" +print s$ +print +""") + +agree("call depth runs out", Preamble & """ +sub deep(n) + a = a + 1 + deep(n + 1) +end sub +deep(0) +""") + +agree("GOSUB depth runs out", Preamble & """ +again: +a = a + 1 +gosub again +""") + +block: + var limits = defaultLimits() + limits.maxInstructions = 777 + agree("the instruction budget runs out part way", Preamble & """ +while 1 + a = a + 1 + cells(a mod 6) = a +wend +""", limits) + +block: + var limits = defaultLimits() + limits.maxWorkUnits = 901 + agree("the work budget runs out part way", Preamble & """ +while 1 + a = a + 1 + b = a \ 3 +wend +""", limits) + +block: + var limits = defaultLimits() + limits.maxPrintEvents = 5 + agree("the print budget runs out part way", Preamble & """ +while 1 + a = a + 1 + print a +wend +""", limits) + +block: + var limits = defaultLimits() + limits.maxStrings = 6 + agree("string storage runs out part way", Preamble & """ +while 1 + s$ = s$ + "x" + a = a + 1 +wend +""", limits) + +agree("wrapping at both ends", Preamble & """ +a = 2147483647 +a = a + 1 +b = -2147483647 - 1 +c = b \ -1 +d = b mod -1 +e = b * -1 +f = -b +x = 32767.5 +x = x + 1 +y = -32768 +y = y - 0.5 +""") + +agree("DATA read in a loop kept in registers", Preamble & """ +a = 0 +b = 0 +while a < 6 + b = b + table(a) + a = a + 1 +wend +c = weights(0) * table(2) + weights(2) +""") + +agree("DATA handed to host code", Preamble & """ +a = gather(table, cells()) +b = gather(weights, cells) +c = burn(40) +d = gather(cells, table()) +e = 1 +""") + +block: + var limits = defaultLimits() + limits.maxInstructions = 400 + agree("host code charging operations until the budget runs out", + Preamble & """ +while 1 + a = a + gather(table, cells()) +wend +""", limits) + +agree("GOSUB inside a sub with many parameters", Preamble & """ +sub many(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) + gosub more + gosub more + a = a + p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9 + p10 + exit sub +more: + p0 = p0 + 1 + p10 = p10 + 100 + b = b + 1 + return +end sub +many(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) +many(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110) +c = a * 2 + b +""") + +agree("select, for, do, and on-goto", Preamble & """ +for i = 1 to 10 step 3 + select case i + case 1, 4 + a = a + i + case 7 to 9 + b = b + i + case else + c = c + i + end select +next +do while d < 5 + d = d + 1 + if d = 3 then exit do +loop +e = 2 +on e goto first, second, third +first: + f = 1 +second: + g = 2 +third: + h = 3 +""") + +block: + # A host function that writes the script's own state from inside a run + # must be seen at once, by both paths, including inside a loop. + proc pokeRun(native: bool): string = + var host = makeHost() + var target: Runtime + discard host.addFunction("poke", 1, + proc(arguments: openArray[int32]): int32 = + target.setGlobal("a", arguments[0] *% 3) + target.setArray("cells", 0, arguments[0]) + 0 + ) + let program = compile(Preamble & """ +while b < 50 + b = b + 1 + a = a + b + c = c + a + cells(0) + if b mod 7 = 0 then d = poke(b) +wend +""", host) + target = initRuntime(program, host) + if native: + discard target.compileNative() + discard target.run() + $target.getGlobal("a") & " " & $target.getGlobal("c") & " " & + $target.getArray("cells", 0) + let plain = pokeRun(false) + let fast = pokeRun(true) + if plain == fast: + echo " ok host code writing globals mid-run" + else: + inc failures + echo &"FAIL host code writing globals mid-run: {plain} then {fast}" + +## Generated programs + +type Generator = object + random: Rand + depth: int + labels: int + +proc pick[T](g: var Generator, items: openArray[T]): T = + ## Picks one item. + items[g.random.rand(0 ..< items.len)] + +proc chance(g: var Generator, percent: int): bool = + ## Reports true about this often. + g.random.rand(0 ..< 100) < percent + +proc literal(g: var Generator): string = + ## A whole-number or fixed-point constant, with the edges favoured. + case g.random.rand(0 .. 9) + of 0: $g.pick([0, 1, -1, 2147483647, -2147483647, 32767, -32768, 13]) + of 1, 2: &"{g.random.rand(-40 .. 40)}.{g.pick([0, 5, 25, 125, 75])}" + else: $g.random.rand(-20 .. 20) + +proc divisor(g: var Generator): string = + ## A divisor that is usually safe, and now and then zero or minus one. + case g.random.rand(0 .. 19) + of 0: "0" + of 1: "-1" + else: $g.pick([2, 3, 4, 5, 7, 8, 16, -3, -8, 1000]) + +proc whole(g: var Generator): string = + ## A whole-number expression of bounded depth, safe to index and divide. + inc g.depth + defer: dec g.depth + if g.depth > 3 or g.chance(35): + case g.random.rand(0 .. 7) + of 0, 1: return $g.random.rand(-20 .. 20) + of 2: return $g.pick([0, 1, -1, 2147483647, -2147483647, 13, 65536]) + of 3, 4, 5: return g.pick(Scalars) + of 6: return "table(" & $g.random.rand(0 ..< TableCells) & ")" + else: return "cells(" & $g.random.rand(0 ..< Cells) & ")" + case g.random.rand(0 .. 11) + of 0 .. 3: + let op = g.pick(["+", "-", "*", "+", "-"]) + "(" & g.whole() & " " & op & " " & g.whole() & ")" + of 4, 5: + let op = g.pick(["\\", "mod"]) + "(" & g.whole() & " " & op & " " & g.divisor() & ")" + of 6: + let op = g.pick(["=", "<>", "<", "<=", ">", ">="]) + "(" & g.whole() & " " & op & " " & g.whole() & ")" + of 7: + let op = g.pick(["and", "or", "xor", "eqv", "imp"]) + "(" & g.whole() & " " & op & " " & g.whole() & ")" + of 8: "(not " & g.whole() & ")" + of 9: "-" & g.whole() + of 10: "twice(" & g.whole() & ")" + else: "pick(" & g.whole() & ", " & g.whole() & ")" + +proc text(g: var Generator): string + +proc numeric(g: var Generator): string + +proc cellIndex(g: var Generator): string = + ## An index that is usually in range and sometimes not. + if g.chance(90): + $g.random.rand(0 ..< Cells) + elif g.chance(50): + "(" & g.whole() & " mod 6)" + else: + g.numeric() + +proc numeric(g: var Generator): string = + ## A numeric expression of bounded depth, either kind. + inc g.depth + defer: dec g.depth + if g.depth > 3 or g.chance(30): + case g.random.rand(0 .. 6) + of 0, 1: return g.literal() + of 2, 3: return g.pick(Scalars) + of 4: return g.pick(Decimals) + of 5: + if g.chance(50): + return "table(" & g.cellIndex() & ")" + return "weights(" & $g.random.rand(0 .. 3) & ")" + else: return "cells(" & g.cellIndex() & ")" + case g.random.rand(0 .. 16) + of 0 .. 4: + let op = g.pick(["+", "-", "*", "+", "-"]) + "(" & g.numeric() & " " & op & " " & g.numeric() & ")" + of 5: + "(" & g.numeric() & " / " & g.pick(["2", "0.5", "-4", "3", "0"]) & ")" + of 6: + "(" & g.numeric() & " / " & g.numeric() & ")" + of 7: + let op = g.pick(["\\", "mod"]) + "(" & g.numeric() & " " & op & " " & g.divisor() & ")" + of 8, 9: + let op = g.pick(["=", "<>", "<", "<=", ">", ">="]) + "(" & g.numeric() & " " & op & " " & g.numeric() & ")" + of 10: + let op = g.pick(["and", "or", "xor", "eqv", "imp"]) + "(" & g.numeric() & " " & op & " " & g.numeric() & ")" + of 11: "(not " & g.numeric() & ")" + of 12: "-" & g.numeric() + of 13: "halve(" & g.numeric() & ")" + of 14: + case g.random.rand(0 .. 3) + of 0: "len(" & g.text() & ")" + of 1: "asc(" & g.text() & ")" + of 2: "(" & g.text() & " = " & g.text() & ")" + else: "(" & g.text() & " <> " & g.text() & ")" + of 15: g.whole() + else: "twice(" & g.whole() & ")" + +proc text(g: var Generator): string = + ## A string expression. + case g.random.rand(0 .. 8) + of 0: "\"" & g.pick(["", "a", "abc", " pad ", "Mixed"]) & "\"" + of 1: g.pick(Texts) + of 2: g.pick(Texts) & " + " & g.pick(Texts) + of 3: "left$(" & g.pick(Texts) & ", " & $g.random.rand(0 .. 4) & ")" + of 4: "mid$(" & g.pick(Texts) & ", " & $g.random.rand(1 .. 4) & ")" + of 5: "str$(" & g.numeric() & ")" + of 6: "words$(" & $g.random.rand(0 ..< TextCells) & ")" + of 7: "mid$(" & g.pick(Texts) & ", " & g.whole() & ", " & g.whole() & ")" + else: "chr$(" & $g.random.rand(60 .. 90) & ")" + +proc statement(g: var Generator, indent: string, room: int): string + +proc body(g: var Generator, indent: string, room: int): string = + ## A few statements. + for _ in 0 ..< g.random.rand(1 .. 4): + result.add(g.statement(indent, room)) + +proc statement(g: var Generator, indent: string, room: int): string = + ## One statement, nesting only while there is room. + let choice = g.random.rand(0 .. (if room > 0: 16 else: 9)) + case choice + of 0 .. 3: + indent & g.pick(Scalars) & " = " & + (if g.chance(80): g.whole() else: g.numeric()) & "\n" + of 4: + indent & g.pick(Decimals) & " = " & g.numeric() & "\n" + of 5: + indent & "cells(" & g.cellIndex() & ") = " & + (if g.chance(70): g.whole() else: g.numeric()) & "\n" + of 6: + indent & g.pick(Texts) & " = " & g.text() & "\n" + of 7: + indent & "words$(" & $g.random.rand(0 ..< TextCells) & ") = " & + g.text() & "\n" + of 8: + indent & "print " & g.numeric() & "; " & g.text() & "\n" + of 9: + indent & g.pick(["bump(" & g.whole() & ")", "gosub tally", + "a = a + 1", "b = b - 1", "c = gather(table, cells())", + "d = gather(weights, cells)", "a = burn(" & g.whole() & ")", + "b = gather(cells, table())", + "wide(" & g.whole() & ", 1, 2, 3, 4, " & g.whole() & + ", 6, 7, 8, " & g.whole() & ", 10)"]) & "\n" + of 10 .. 12: + let condition = g.numeric() + var text = indent & "if " & condition & " then\n" & + g.body(indent & " ", room - 1) + if g.chance(50): + text.add(indent & "else\n" & g.body(indent & " ", room - 1)) + text & indent & "end if\n" + of 13, 14: + let counter = g.pick(Scalars) + indent & "for " & counter & " = 0 to " & $g.random.rand(0 .. 12) & + "\n" & g.body(indent & " ", room - 1) & indent & "next\n" + else: + let counter = g.pick(Scalars) + indent & "while " & counter & " < " & $g.random.rand(-5 .. 30) & "\n" & + indent & " " & counter & " = " & counter & " + 1\n" & + g.body(indent & " ", room - 1) & indent & "wend\n" + +proc generated(seed: int64): string = + ## Builds one program out of everything the language offers. + var g = Generator(random: initRand(seed)) + result = Preamble + for name in Scalars: + if g.chance(70): + result.add(name & " = " & g.literal() & "\n") + for name in Decimals: + if g.chance(70): + result.add(name & " = " & g.literal() & "\n") + result.add(g.body("", 2)) + result.add(g.body("", 2)) + result.add("end\n") + result.add("tally:\n c = c + " & g.literal() & "\n return\n") + result.add("sub bump(n)\n d = d + n\n" & g.body(" ", 1) & + " if n < 3 then bump(n + 1)\nend sub\n") + # More parameters than a return copies in straight-line code, handed + # back and forth through GOSUB, which shares them with the caller. + result.add("sub wide(p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)\n" & + " gosub widen\n gosub widen\n" & + " c = c + p0 + p3 + p7 + p10\n exit sub\n" & + "widen:\n p0 = p0 + 1\n p10 = p10 + p9\n p5 = p5 - p1\n" & + " return\nend sub\n") + +var + tried = 0 + disagreed = 0 + outcomes: CountTable[string] +for seed in 1'i64 .. 1500'i64: + let source = generated(seed) + var limits = defaultLimits() + limits.maxCallDepth = 16 + limits.maxInstructions = [300'i64, 5_000, 200_000][int(seed mod 3)] + limits.maxStrings = 64 + try: + discard compile(source, makeHost(), limits) + except BasicError: + continue + inc tried + let plain = observe(source, false, limits, 3) + for line in plain.splitLines: + if line.startsWith("raised "): + outcomes.inc(line[0 ..< min(line.len, 60)]) + elif line.startsWith("stats "): + outcomes.inc("finished") + let fast = observe(source, true, limits, 3) + if plain != fast: + inc disagreed + if disagreed <= 2: + echo &"FAIL generated seed {seed}" + echo source + let plainLines = plain.splitLines + let fastLines = fast.splitLines + for index in 0 ..< max(plainLines.len, fastLines.len): + let left = if index < plainLines.len: plainLines[index] else: "" + let right = if index < fastLines.len: fastLines[index] else: "" + if left != right: + echo &" interpreted {left}" + echo &" native {right}" +if disagreed > 0: + inc failures + echo &"FAIL {disagreed} of {tried} generated programs disagreed" +else: + echo &" ok {tried} generated programs agree" +for outcome, count in outcomes: + echo &" {count:>5} {outcome}" +echo &" {handed} of {executed} instructions handed back to interpreter code" + +if failures > 0: + quit(&"{failures} native checks failed") +echo &"whole programs compiled natively: {compiled}" +echo "compiled programs are indistinguishable from interpreted ones"