diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index e8d5fa3..430a595 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -23,7 +23,7 @@ jobs: pip install -r requirements.txt - name: Test with pytest run: | - pytest -v --doctest-modules + pytest -v --doctest-modules --ignore alt/scheme/ribbit - name: Lint with flake8 run: | pip install flake8 diff --git a/alt/README.md b/alt/README.md index 59b5edb..7eaf67d 100644 --- a/alt/README.md +++ b/alt/README.md @@ -8,16 +8,29 @@ See each module for instructions. ## Enhanced chips -Four alternative implementations use more or less chip hardware to make programs run faster, or to fit larger programs in ROM: +Four alternative implementations use more or less chip hardware to make programs run faster, or to +fit larger programs in ROM: -[alt/sp.py](sp.py) adds instructions for pushing/popping values to/from the stack, making programs more compact. +[alt/sp.py](sp.py) adds instructions for pushing/popping values to/from the stack, making programs +more compact and efficient. -[alt/threaded.py](threaded.py) adds lightweight CALL/RTN instructions, enabling a very compact "threaded interpreter" translation, which runs a little slower. +[alt/threaded.py](threaded.py) adds lightweight CALL/RTN instructions, enabling a very compact +"threaded interpreter" translation, which runs a little slower. -[alt/shift.py](shift.py) adds a "shiftr" instruction, and rewrites "push constant 16; call Math.divide" to use it instead; also a more efficient Math.multiply using shiftr. +[alt/shift.py](shift.py) adds a "shiftr" instruction, and rewrites +"push constant 16; call Math.divide" to use it instead; also a more efficient Math.multiply using shiftr. [alt/eight.py](eight.py) is, finally, a _smaller_ CPU, by using an 8-bit ALU and 2 cycles per instruction. +[alt/big.py](big.py) has a single, flat memory space, with maximum RAM and the ability to read +data from ROM (and code from RAM.) This is much more flexible and realistic, but adds a cycle to +fetch each instruction from the shared memory system. Moving static data to ROM can dramtically +improve code size and performance, but because the computer uses character-mode graphics, these +metrics don't provide a direct comparison (even if you did port the VM and OS, which I haven't.) +This architecture is intended to support more sophisticated languages (e.g. BASIC, Scheme, or Forth), +and interactive programming. + + ## Enhanced compiler/translators These implementations all use the standard CPU, and try to generate more efficient code for it: @@ -30,6 +43,12 @@ local variables and expression evaluation, reserving the stack only for subrouti [alt/reduce.py](reduce.py) adds an optimization phase after parsing and before the normal compiler runs, which replaces certain function calls with lower-overhead "reduced" alternatives. + +## Alternative languages + +[alt/scheme](scheme/) provides a compiler and REPL for the Scheme language (circa R4RS), using the "big" architecture. + + ## Results | Location | Nands | ROM size | Cycles per frame | Cycles for init | @@ -39,10 +58,12 @@ replaces certain function calls with lower-overhead "reduced" alternatives. | [alt/threaded.py](threaded.py) | 1,549 (+23%) | 8,100 (-68%) | 49,600 (+20%) | 173,750 (+34%) | | [alt/shift.py](shift.py) | 1,311 (+4%) | 26,050 (+1%) | 19,800 (-52%) | _same_ | | [alt/eight.py](eight.py) | 1,032 (-18%) | _same_ | +100% | +100% | +| [alt/big.py](big.py) | 1,448 (+14%) | ? | ? | ? | | [alt/lazy.py](lazy.py) | _same_ | 23,650 (-8%) | 37,300 (-10%) | 111,000 (-14%) | -| [alt/reg.py](reg.py) | _same_ | 20,900 (-19%) | 19,150 (-54%) | 59,000 (-54%) | +| [alt/reg.py](reg.py) | _same_ | 18,200 (-29%) | 12,450 (-70%) | 55,250 (-57%) | | [alt/reduce.py](reduce.py) | _same_ | 27,350 (+6.5%) | 20,300 (-51%) | _same_ | + **ROM Size** is the total number of instructions in ROM when Pong is compiled and translated from the Jack source. diff --git a/alt/big.py b/alt/big.py new file mode 100755 index 0000000..d27a455 --- /dev/null +++ b/alt/big.py @@ -0,0 +1,487 @@ +#! /usr/bin/env python + +"""A computer with a single 16-bit address space for ROM and RAM. + +Uses the same ISA and assembler as the normal Hack CPU, with a few extensions. + +Both instructions and data can be read from any address. *Note: because instructions and +data share the single memory bus, it now takes 2 cycles to fetch and execute most if not +all instructions.* This is more or less authentic to early integrated CPUs, which were +designed to minimize pinout while making effective use of whatever memory the customer +was able to afford. + +Writes to ROM addresses are ignored. + +A small portion of the RAM is reserved for screen buffer and I/O: +- 2000 words to hold 80x25 16-bit characters +- Keyboard and TTY in the same 2048-word "page". +- 47 other words are unused. + +Layout considerations: +- ROM is only large enough to fit a scheme/basic/forth interpreter (8K?) +- Screen buffer and I/O: 2K +- RAM fills as much of the rest of 64K as possible +- The low-memory page is RAM, for convenient access. +- The ROM lives in the 15-bit addressable range, so a code address can always be loaded in one cycle +- "negative" addresses are a uniform block of RAM, useful for heap + +| Address Range | Size (words) | Storage | Contents | +| 0x0000–0x07FF | 2K | RAM | Temporaries, "registers", stack, etc. | +| 0x0800–0x0FFF | 2K | RAM | Screen buffer and I/O | +| 0x1000–0x7FFF | 28K | ROM | Code: boot/runtime; data | +| 0x8000–0xFFFF | 32K | RAM | Heap or other large blocks | + +Note: it's also possible to treat all negative values as addresses in a continous block of RAM +starting at 0x8000 = -32768. In fact, this range extends all the way to the bottom of the ROM +at 0x1000 = 4096. + +TODO: make the size of the ROM configurable, so you can trade off heap space vs runtime size. +That means synthesizing the logic to overlay the right address range, so maybe you just select +one of a few available sizes, e.g. 4K, 12K, 28K? + +"Character-mode" graphics make more efficient use of memory, with only 2k allocated to the screen +as opposed to 8K for a similar number of pixels (assuming 9-point font.) To keep things simple and +quick, each 7-bit character is stored in a whole 16-bit word. No shifting or masking is necessary +to draw a single character on the screen. + +For authentic Macintosh fonts, see https://archive.org/details/AppleMacintoshSystem753. +""" + +from nand import chip, lazy, RAM, ROM, Input, Output, DFF +import nand.syntax +from nand.vector import unsigned +from project_01 import And, Or, Not +from project_03 import Mux, Mux16, Register, PC, ALU +from nand.solutions import solved_06 +from alt.threaded import Eq16 + +SCREEN_BASE = 0x0800 +KEYBOARD_ADDR = 0x0FFF +ROM_BASE = 0x1000 +HEAP_BASE = 0x8000 # Note: too big for "@-" instruction +HEAP_TOP = 0xFFFF # Note: too big for "@-" instruction + + +# +# Components: +# + +@chip +def FlatMemory(inputs, outputs): + """The same interface as MemorySystem, but also maps the ROM and extends address to the full 16 bits. + """ + + in_ = inputs.in_ + load = inputs.load + address = inputs.address + + # addresses 0x10-- through 0x7F-- are in the ROM: + # - high bits 0001–0111 (0001 0000 0000 0000 to 0111 1111 1111 1111) + # - or, 0..1 .... .... .... + # - or, 1 <= (address >> 12) < 8 + is_rom = And(a=Not(in_=address[15]).out, + b=Or(a=Or(a=address[14], + b=address[13]).out, + b=address[12]).out).out + + is_io = Eq16(a=address, b=KEYBOARD_ADDR).out + + ram = RAM(16)(in_=in_, load=load, address=address) # Fully 64K; partially hidden by the ROM + rom = ROM(15)(address=address) # Based at 0 and sized to 32K, but partially hidden by RAM + # screen = RAM(10) # Separate RAM would make life easier for the harness? + keyboard = Input() + tty = Output(in_=in_, load=And(a=load, b=is_io).out) + + outputs.out = Mux16(sel=is_io, + a=Mux16(sel=is_rom, a=ram.out, b=rom.out).out, + b=keyboard.out).out + outputs.tty_ready = tty.ready + + +# TODO: move this to solved_05 as the standard impl +@chip +def DecodeALU(inputs, outputs): + """Inspect the bits of an instruction word and determine if it involves the ALU and if so + what the control signals should be. + + When `is_alu_instr` is high, the rest of the signals should be wired to the ALU and PC. + + This decoding is intended to be independent of architectural concerns such as cycle timing, + memory mapping, etc., so it could be used in a variety of implementations. + + Note: there's almost no logic here, so just a handful of gates, but breaking out the signals + and providing clear names for them can make the various CPU implementations easier to follow. + """ + + instruction = inputs.instruction + + i, _, _, a, c5, c4, c3, c2, c1, c0, da, dd, dm, _, _, _ = [instruction[j] for j in reversed(range(16))] + + # If low, then all the other output signals should be ignored: + outputs.is_alu_instr = i + + # ALU control: + outputs.mem_to_alu = a # if low, the ALU gets the value of A; if high, the value from memory + outputs.zx = c5 + outputs.nx = c4 + outputs.zy = c3 + outputs.ny = c2 + outputs.f = c1 + outputs.no = c0 + + # ALU Destinations: + # Note: these are all low when the ALU is not in effect, so it's safe to use them directly + outputs.alu_to_a = And(a=i, b=da).out + outputs.alu_to_d = And(a=i, b=dd).out + outputs.alu_to_m = And(a=i, b=dm).out + + +# TODO: move this to solved_05 as the standard impl +@chip +def DecodeJump(inputs, outputs): + """Inspect the bits of an instruction word and the control signals from the ALU and determine + if a branch should be taken. + + Note: if the instruction isn't an ALU instruction, then the output is low. + """ + + instruction = inputs.instruction + ng = inputs.ng + zr = inputs.zr + + i, _, _, _, _, _, _, _, _, _, _, _, _, jlt, jeq, jgt = [instruction[j] for j in reversed(range(16))] + + # TODO: this seems like a lot of gates to transform 5 bits of input into one bit of output. + # Hmm, is 16 a lot? + outputs.jump = And(a=i, + b=Or(a=Or(a=And(a=jlt, b=ng).out, + b=And(a=jeq, b=zr).out).out, + b=And(a=jgt, b=And(a=Not(in_=ng).out, b=Not(in_=zr).out).out).out).out).out + + +@chip +def IdlableCPU(inputs, outputs): + """Same as the standard CPU, plus an 'idle' input that suspends all state updates.""" + + inM = inputs.inM # M value input (M = contents of RAM[A]) + instruction = inputs.instruction # Instruction for execution + reset = inputs.reset # Signals whether to re-start the current + # program (reset==1) or continue executing + # the current program (reset==0). + # Extra for fetch/execute cycles: + idle = inputs.idle # When set, *don't* update any state + + decode = DecodeALU(instruction=instruction) + + is_imm = Not(in_=decode.is_alu_instr).out + + not_idle = Not(in_=idle).out + + alu = lazy() + a_reg = Register(in_=Mux16(a=instruction, b=alu.out, sel=decode.is_alu_instr).out, + load=And(a=not_idle, b=Or(a=is_imm, b=decode.alu_to_a).out).out) + d_reg = Register(in_=alu.out, + load=And(a=not_idle, b=decode.alu_to_d).out) + + jump = DecodeJump(instruction=instruction, ng=alu.ng, zr=alu.zr).jump + + pc = PC(in_=a_reg.out, load=And(a=not_idle, b=jump).out, inc=not_idle, reset=reset) + alu.set(ALU(x=d_reg.out, y=Mux16(a=a_reg.out, b=inM, sel=decode.mem_to_alu).out, + zx=decode.zx, nx=decode.nx, zy=decode.zy, ny=decode.ny, f=decode.f, no=decode.no)) + + + outputs.outM = alu.out # M value output + outputs.writeM = decode.alu_to_m # Write to M? + outputs.addressM = a_reg.out # Address in data memory (of M) (latched) + outputs.pc = pc.out # address of next instruction (latched) + + +@chip +def BigComputer(inputs, outputs): + """A computer with the standard CPU, but mapping RAM, ROM, and I/O into the same large, flat + memory space. + + In every even (fetch) cycle, an instruction is read from memory and stored in an extra Register. + In odd (execute) cycles, memory and cpu state are updated as required by the instruction. + + Note: on start/reset, instructions from address 0 are read. Since a zero-value instruction + just loads 0 into A, we effectively execute 2K no-op "@0" instructions before reaching the + first actual ROM address. + + TODO: some instructions don't require access to the memory; they don't read or write from/to M. + In that case, we could fetch and execute in a single cycle if the CPU exposed that info or took + over control of the cycles. It looks like possibly as much as 50% of all instructions could + execute in one cycle for 25% speedup. On the other hand, it would complicate tests somewhat + unless we add "performance counters" to the CPU to keep track. + """ + + reset = inputs.reset + + # A DFF to split each pair of cycles into two halves: + # fetch is True in the first half-cycle, when the instruction is fetched from memory. + # execute is True in the second half-cycle, when any read/write operations are done. + # The DFF stores execute (= !fetch), so that we start in fetch. + half_cycle = lazy() + fetch = Not(in_=half_cycle.out).out + half_cycle.set(DFF(in_=Mux(a=fetch, b=0, sel=reset).out)) + execute = half_cycle.out + + cpu = lazy() + + addr = Mux16(a=cpu.pc, b=cpu.addressM, sel=execute).out + + mem = FlatMemory(in_=cpu.outM, + load=And(a=execute, b=cpu.writeM).out, + address=addr) + + instr_reg = Register(in_=mem.out, load=fetch) + + # TODO: Set an instruction code that results in less computation during simulation, or is + # there a way to do that that's more realistic? Maybe just leave the instruction unchanged + # and disable all state updates; if the ALUs inputs don't change, it consumes no power? + # Does that apply to (vector) simulation? + cpu.set(IdlableCPU(inM=mem.out, + instruction=instr_reg.out, + reset=reset, + idle=fetch)) + + # HACK: need some dependency to force the whole thing to be synthesized. + # Exposing the PC also makes it easy to observe what's happening in a dumb way. + outputs.pc = cpu.pc + + # HACK: similar issues, but in this case it's just the particular component that + # needs to be forced to be included. + outputs.tty_ready = mem.tty_ready + + outputs.fetch = fetch # exposed so debuggers can track half-cycles + # DEBUG: + # # outputs.execute = execute + # outputs.addr = addr + # # outputs.addressM = cpu.addressM + # outputs.writeM = cpu.writeM + # outputs.outM = cpu.outM + # outputs.instr = instr_reg.out + + +# +# Assembler: +# + +import re + + +BUILTIN_SYMBOLS = { + **{ + "SCREEN": SCREEN_BASE, + "KEYBOARD": KEYBOARD_ADDR, + "ROM": ROM_BASE, + "HEAP_MINUS_ONE": HEAP_BASE-1, # doesn't fit in 15 bits, so this points to the address before + }, + **solved_06.register_names(16) +} + + +def parse_op(string, symbols={}): + """Handle 16-bit constants (e.g. #-10 or #0xFF00); used for data to be read from ROM. + """ + m = re.match(r"#(-?((0x[0-9a-fA-F]+)|([1-9][0-9]*)|0))", string) + if m: + value = eval(m.group(1)) + if value < -32768 or value > 65535: + raise Exception(f"Constant value out of range: {value} ({string})") + return unsigned(value) + else: + return solved_06.parse_op(string, symbols) + + +def assemble(f, min_static=16, max_static=1023, builtins=BUILTIN_SYMBOLS): + """Standard assembler, except: shift the ROM base address, symbols for other base addresses, and 16-bit data.""" + return solved_06.assemble(f, + parse_op=parse_op, + min_static=min_static, + max_static=max_static, + start_addr=ROM_BASE, + builtins=builtins) + + +# +# Harness: +# + +import computer +import pygame.image +import sys +import time + + +EVENT_INTERVAL = 1/10 +DISPLAY_INTERVAL = 1/20 # Note: screen update is pretty slow at this point, so no point in trying for a higher frame rate. +CYCLE_INTERVAL = 1/1.0 # How often to update the cycle and frame counters; a bit longer so they doesn't bounce around too much + +CYCLES_PER_CALL = 100 # Number of cycles to run in the tight loop (when not tracing) + + +def run(program, chip=BigComputer, simulator="codegen", name="Flat!", font="monaco-9", halt_addr=None, trace=None, verbose_tty=True, meters=None): + """Run with keyboard and text-mode graphics.""" + + # TODO: font + + # A little sanity checking: + if len(program) > HEAP_BASE: + raise Exception(f"Program too large for ROM: {(len(program) - ROM_BASE)/1024:0.1f}K > {(HEAP_BASE - ROM_BASE)//1024}K") + elif any(b != 0 for b in program[:ROM_BASE]): + print("WARNING: non-zero words found in the program image below ROM_BASE; this memory is hidden by low RAM") + + computer = nand.syntax.run(chip, simulator=simulator) + + computer.init_rom(program) + + # Jump over low memory that we might be using for debugging: + computer.poke(0, ROM_BASE) + computer.poke(1, parse_op("0;JMP")) + + def run_trace(): + try: + trace(computer, cycles) + except: + print(f"exception in trace: {sys.exc_info()[1]}") + + + kvm = TextKVM(name, 80, 25, 6, 10, "alt/big/Monaco9.png") + + # TODO: use computer.py's "run", for many more features + + last_cycle_time = last_event_time = last_display_time = last_frame_time = now = time.monotonic() + halted = False + + last_cycle_count = cycles = 0 + while True: + if not halted and computer.pc == halt_addr: + halted = True + print(f"\nHalted after {cycles:,d} cycles\n") + if trace is not None: + run_trace() + + + if halted: + time.sleep(EVENT_INTERVAL) + + elif trace is None: + computer.ticktock(CYCLES_PER_CALL) + cycles += CYCLES_PER_CALL + + else: + computer.ticktock() + cycles += 1 + + if computer.fetch and trace is not None: + run_trace() + + if cycles % CYCLES_PER_CALL == 0 or halted: + now = time.monotonic() + + # A few times per second, process events and update the display: + if now >= last_event_time + EVENT_INTERVAL: + last_event_time = now + key = kvm.process_events() + computer.set_keydown(key or 0) + + if now >= last_display_time + DISPLAY_INTERVAL: + last_display_time = now + kvm.update_display(lambda x: computer.peek(SCREEN_BASE + x)) + + + tty_char = computer.get_tty() + if tty_char: + if 32 < tty_char <= 127: + if not verbose_tty: + print(chr(tty_char), end="", flush=True) + else: + print(f"TTY: {repr(chr(tty_char))} ({tty_char})") + else: + print(f"TTY: ({tty_char:6d}; 0x{unsigned(tty_char):04x})") + + + msgs = [] + msgs.append(f"{cycles/1000:0.1f}k cycles") + cps = (cycles - last_cycle_count)/(now - last_cycle_time) + msgs.append(f"{cps/1000:0,.1f}k/s") + msgs.append(f"@{computer.pc}") + if meters: + msgs.extend(meters(computer, cycles)) + pygame.display.set_caption(f"{name}: {'; '.join(msgs)}") + + last_cycle_time = now + last_cycle_count = cycles + + +class TextKVM(computer.KVM): + """Keyboard and display, displaying characters using a set of baked-in glyphs. + + Each word of the screen buffer stores a single 16-bit character. + """ + + def __init__(self, title, char_width, char_height, glyph_width, glyph_height, bitmap_path): + computer.KVM.__init__(self, title, char_width*glyph_width, char_height*glyph_height) + + self.char_width = char_width + self.char_height = char_height + self.glyph_width = glyph_width + self.glyph_height = glyph_height + + self.glyph_sheet = pygame.image.load(bitmap_path) + + + def update_display(self, get_chars): + self.screen.fill(computer.COLORS[0]) + + for y in range(0, self.char_height): + for x in range(0, self.char_width): + char = get_chars(y*self.char_width + x) + self.render(x, y, char) + + pygame.display.flip() + + + def render(self, x, y, c): + g_x = (c & 0x0F)*self.glyph_width + g_y = (c >> 4)*self.glyph_height + self.screen.blit(self.glyph_sheet, + dest=(x*self.glyph_width, + y*self.glyph_height), + area=pygame.Rect(g_x, + g_y, + self.glyph_width, + self.glyph_height)) + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Run assembly source with text-mode display and keyboard") + parser.add_argument("path", help="Path to source (.asm)") + parser.add_argument("--simulator", action="store", default="codegen", help="One of 'vector' (slower, more precise); 'codegen' (faster, default); 'compiled' (experimental)") + # parser.add_argument("--trace", action="store_true", help="(VM/Jack-only) print cycle counts during initialization. Note: runs almost 3x slower.") + # parser.add_argument("--print", action="store_true", help="(VM/Jack-only) print translated assembly.") + # TODO: "--debug" showing opcode-level trace. Breakpoints, stepping, peek/poke? + # parser.add_argument("--no-waiting", action="store_true", help="(VM/Jack-only) substitute a no-op function for Sys.wait.") + # parser.add_argument("--max-fps", action="store", type=int, help="Experimental! (VM/Jack-only) pin the game loop to a fixed rate, approximately (in games that use Sys.wait).\nMay or may not work, depending on the translator.") + # TODO: "--max-cps"; limit the clock speed directly. That will allow different chips to be compared (in a way). + # TODO: "--headless" with no UI, with Keyboard and TTY connected to stdin/stdout + + args = parser.parse_args() + + print(f"Reading assembly from file: {args.path}") + with open(args.path, mode='r') as f: + prg, symbols, statics = assemble(f) + + prg_size = len(prg) - ROM_BASE + max_size = HEAP_BASE - ROM_BASE + print(f"Size in ROM: {prg_size:0,d} ({100*prg_size/max_size:0.1f}% of {max_size//1024}K)") + print(f"symbols: {symbols}") + print(f"statics: {statics}") + + run(program=prg, simulator=args.simulator, halt_addr=symbols["halt"]) + + +if __name__ == "__main__": + main() diff --git a/alt/big/Echo.asm b/alt/big/Echo.asm new file mode 100644 index 0000000..37581bf --- /dev/null +++ b/alt/big/Echo.asm @@ -0,0 +1,99 @@ +// Read characters from the keyboard and echo them to the screen, starting at the top line. +// - backspace (129) will delete characters to the left, until the beginning of the current line +// - newline (128) moves the insertion point to the beginning of the next line +// - typing off the end of the line wraps to the next line +// - when continuing past the bottom line, the previous 24 lines are "scrolled" up, and the +// top line is discarded +// - esc (140) stops accepting input, tells the simulator the program is complete +// +// Future: make it a free-form editor, showing a window of 25 lines from a 32K buffer? + +// +// Initialize some state: +// + +// PREVIOUS_KEY: key code that was down on previous iteration +@PREVIOUS_KEY +M=0 +// INSERTION_COLUMN: insertion point column/2 +@INSERTION_COLUMN +M=0 +// INSERTION_HIGH: insertion point column odd/even flag (i.e. the low bit) +@INSERTION_HIGH +M=0 +// CURRENT_LINE: current line +@CURRENT_LINE +M=0 + + +// +// Infinite loop: +// + +(loop) +// Read current key down: +@KEYBOARD +D=M + +// Compare with previous: +@R0 +M=D +@PREVIOUS_KEY +D=M-D +@loop +D;JEQ +@R0 +D=M +// Store current key: +@PREVIOUS_KEY +M=D + +// Compare with zero (no key down): +@loop +D;JEQ + +// Compare with ESC: +@140 +D=A +@PREVIOUS_KEY +D=M-D +@halt +D;JEQ + +// TODO: backspace (@129) +// TODO: newline (@128) + +// R0 = address of insertion point: +// TODO: half-word +@INSERTION_COLUMN +D=M +@SCREEN +D=D+A +@R0 +M=D +// Write current key to the screen +@PREVIOUS_KEY +D=M +@R0 +A=M +M=D +// Also echo to the TTY for debug purposes +@KEYBOARD +M=D + +// Move to the right: +// TODO: half-word at a time +@INSERTION_COLUMN +M=M+1 + +@loop +0;JMP + + +// +// Halt loop: required by the harness, but also a place to go on ESC +// + +(halt) +@halt +0;JMP diff --git a/alt/big/Monaco9.png b/alt/big/Monaco9.png new file mode 100644 index 0000000..5f8ae21 Binary files /dev/null and b/alt/big/Monaco9.png differ diff --git a/alt/big/Output.asm b/alt/big/Output.asm new file mode 100644 index 0000000..1e66e26 --- /dev/null +++ b/alt/big/Output.asm @@ -0,0 +1,816 @@ +// Write characters to the screen buffer: + +// A B +// +// 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F +// 00 +// 10 +// 20 ! " # $ % & ' ( ) * + , - . / +// 30 0 1 2 3 4 5 6 7 8 9 : ; < = > ? +// 40 @ A B C D E F G H I J K L M N O +// 50 P Q R S T U V W X Y Z [ / ] ^ _ +// 60 ` a b c d e f g h i j k l m n o +// 70 p q r s t u v w x y z { | } ~ +// +// ... +// +// C D + +// Print "A", "B", "C", "D in the four corners: +@65 +D=A +@SCREEN +M=D +@66 +D=A +@2127 +M=D +@67 +D=A +@3968 +M=D +@68 +D=A +@4047 +M=D + +(copy_start) +// DST = start of third row of screen buffer +@SCREEN +D=A +@240 +D=D+A +@DST +M=D +// SRC = @table_start (start of the table in ROM) +@table_start +D=A +@SRC +M=D + +(copy_loop) +// D = MEM[SRC++] +@SRC +M=M+1 +A=M-1 +D=M +// MEM[DST++] = D +@DST +M=M+1 +A=M-1 +M=D + +// Check for end of table: +@SRC +D=M +@table_end +D=D-A +@copy_loop +D;JLE + + +(halt) +@halt +0;JMP + +// 80 words per line, for a header and then 8 lines for the lower half of the 8-bit table: +(table_start) +// header +#0 +#0 +#0 +#0 +#0x30 +#0x30 +#0 +#0x30 +#0x31 +#0 +#0x30 +#0x32 +#0 +#0x30 +#0x33 +#0 +#0x30 +#0x34 +#0 +#0x30 +#0x35 +#0 +#0x30 +#0x36 +#0 +#0x30 +#0x37 +#0 +#0x30 +#0x38 +#0 +#0x30 +#0x39 +#0 +#0x30 +#0x41 +#0 +#0x30 +#0x42 +#0 +#0x30 +#0x43 +#0 +#0x30 +#0x44 +#0 +#0x30 +#0x45 +#0 +#0x30 +#0x46 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x00–0F: +#0 +#0x30 +#0x30 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x10–1F: +#0 +#0x31 +#0x30 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x20–2F: +#0 +#0x32 +#0x30 +#0 +#0 +#0x20 +#0 +#0 +#0x21 +#0 +#0 +#0x22 +#0 +#0 +#0x23 +#0 +#0 +#0x24 +#0 +#0 +#0x25 +#0 +#0 +#0x26 +#0 +#0 +#0x27 +#0 +#0 +#0x28 +#0 +#0 +#0x29 +#0 +#0 +#0x2A +#0 +#0 +#0x2B +#0 +#0 +#0x2C +#0 +#0 +#0x2D +#0 +#0 +#0x2E +#0 +#0 +#0x2F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x30–3F: +#0 +#0x33 +#0x30 +#0 +#0 +#0x30 +#0 +#0 +#0x31 +#0 +#0 +#0x32 +#0 +#0 +#0x33 +#0 +#0 +#0x34 +#0 +#0 +#0x35 +#0 +#0 +#0x36 +#0 +#0 +#0x37 +#0 +#0 +#0x38 +#0 +#0 +#0x39 +#0 +#0 +#0x3A +#0 +#0 +#0x3B +#0 +#0 +#0x3C +#0 +#0 +#0x3D +#0 +#0 +#0x3E +#0 +#0 +#0x3F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x40–4F: +#0 +#0x34 +#0x30 +#0 +#0 +#0x40 +#0 +#0 +#0x41 +#0 +#0 +#0x42 +#0 +#0 +#0x43 +#0 +#0 +#0x44 +#0 +#0 +#0x45 +#0 +#0 +#0x46 +#0 +#0 +#0x47 +#0 +#0 +#0x48 +#0 +#0 +#0x49 +#0 +#0 +#0x4A +#0 +#0 +#0x4B +#0 +#0 +#0x4C +#0 +#0 +#0x4D +#0 +#0 +#0x4E +#0 +#0 +#0x4F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x50–5F: +#0 +#0x35 +#0x30 +#0 +#0 +#0x0050 +#0 +#0 +#0x0051 +#0 +#0 +#0x0052 +#0 +#0 +#0x0053 +#0 +#0 +#0x0054 +#0 +#0 +#0x0055 +#0 +#0 +#0x0056 +#0 +#0 +#0x0057 +#0 +#0 +#0x0058 +#0 +#0 +#0x0059 +#0 +#0 +#0x005A +#0 +#0 +#0x005B +#0 +#0 +#0x005C +#0 +#0 +#0x005D +#0 +#0 +#0x005E +#0 +#0 +#0x005F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x60–6F: +#0 +#0x36 +#0x30 +#0 +#0 +#0x60 +#0 +#0 +#0x61 +#0 +#0 +#0x62 +#0 +#0 +#0x63 +#0 +#0 +#0x64 +#0 +#0 +#0x65 +#0 +#0 +#0x66 +#0 +#0 +#0x67 +#0 +#0 +#0x68 +#0 +#0 +#0x69 +#0 +#0 +#0x6A +#0 +#0 +#0x6B +#0 +#0 +#0x6C +#0 +#0 +#0x6D +#0 +#0 +#0x6E +#0 +#0 +#0x6F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +// 0x70–7F: +#0 +#0x37 +#0x30 +#0 +#0 +#0x70 +#0 +#0 +#0x71 +#0 +#0 +#0x72 +#0 +#0 +#0x73 +#0 +#0 +#0x74 +#0 +#0 +#0x75 +#0 +#0 +#0x76 +#0 +#0 +#0x77 +#0 +#0 +#0x78 +#0 +#0 +#0x79 +#0 +#0 +#0x7A +#0 +#0 +#0x7B +#0 +#0 +#0x7C +#0 +#0 +#0x7D +#0 +#0 +#0x7E +#0 +#0 +#0x7F +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 +#0 + +(table_end) diff --git a/alt/big/System7.5.3.png b/alt/big/System7.5.3.png new file mode 100644 index 0000000..c8de461 Binary files /dev/null and b/alt/big/System7.5.3.png differ diff --git a/alt/compare.py b/alt/compare.py index d704427..efac919 100755 --- a/alt/compare.py +++ b/alt/compare.py @@ -16,6 +16,7 @@ from alt.shift import SHIFT_PLATFORM from alt.sp import SP_PLATFORM from alt.threaded import THREADED_PLATFORM +import alt.big def main(): std = measure(BUNDLED_PLATFORM) @@ -34,6 +35,8 @@ def main(): # Note: currently the eight-bit CPU doesn't run correctly in the "codegen" simulator, so it's # a little painful to measure its performance directly. However, by design it takes exactly # two cycles per instruction, so we can just report that with a relatively clear conscience. + print_relative_result("alt/big.py", std, (gate_count(alt.big.BigComputer)['nands'], std[1], std[2]*2, std[3]*2)) # Cheeky + # Similar def print_result(name, t): diff --git a/alt/reg.py b/alt/reg.py index 095ba34..f3b1d31 100755 --- a/alt/reg.py +++ b/alt/reg.py @@ -30,7 +30,7 @@ Other differences: -- Subroutine results are stored in r0 (aka @5) +- Subroutine results are stored in @12 (aka r7) - Return addresses are stored at @4 (aka THAT) - Each call site just pushes arguments, stashes the return address and then jumps to the "function" address. It's up to each function to do any saving of registers and adjustment of the stack @@ -39,8 +39,29 @@ These changes mean that the debug/trace logging done by some test cases doesn't always show the correct arguments, locals, return addresses, and result values. + +Extensions: + +Two additional primitives are provided, to support dynamic dispatch (that is, storing the address +of a function or method and invoking it later): + +- Jack.symbol(): address of the given label. +- Jack.invoke(ptr): call the function referred to by the pointer. + +For example, this code sequence: + + var int fooPtr; // the type doesn't matter + let fooPtr = Jack.symbol("main.foo"); + ... + do Jack.invoke(fooPtr); + +has the same effect as this simple call: + + do Main.foo(); """ + +from collections import Counter import itertools from os import name from typing import Dict, Generic, List, NamedTuple, Optional, Sequence, Set, Tuple, TypeVar, Union @@ -52,10 +73,31 @@ from nand.solutions.solved_11 import SymbolTable, VarKind +OPTIMIZE_LEAF_FUNCTIONS = True +"""If True, a simpler call/return sequence is used for functions that do not need to manipulate the +stack (because they don't call any subroutines). +Saves ~30 instructions per leaf function in ROM, plus another ~30 at runtime. For small functions like +Math.min/max/abs, this might save ~50% of the space and ~70% of the time. +Possibly makes tracing/debugging confusing or useless in these functions. +""" + +PRINT_LIVENESS = False +"""Print each function before assigning variables to registers. +Each statement is annotated with the set of variables which are 'live' at the beginning of +that statement. A live variable contains a value that will be used later; it must not be overwritten +at that point.""" + +PRINT_IR = False +"""Print each function immediately before emitting code. +Variables have been assigned to concrete locations, and each statement represents a unit +of work for which code can be generated directly. +""" + + # IR # # A simplified AST for statements and expressions: -# - no expressions; each statement does a single calculation +# - no nested expressions; each statement does a single calculation # - if and while are still here, to simplify control-flow analysis # - separate representations for local variables, which are not at first assigned to a specific # location, and other variables, for which the location is fixed in the symbol table. @@ -76,7 +118,7 @@ class Eval(NamedTuple): """Evaluate an expression and store the result somewhere.""" - dest: "Local" + dest: "Local" # Actually Reg or Static after rewriting expr: "Expr" class IndirectWrite(NamedTuple): @@ -85,7 +127,7 @@ class IndirectWrite(NamedTuple): value: "Value" class Store(NamedTuple): - """Store a value to a location identified by segment and index.""" + """Store a value to a location identified by segment and index (i.e. argument or local).""" location: "Location" value: "Value" @@ -104,8 +146,8 @@ class While(NamedTuple): body: Sequence["Stmt"] class Return(NamedTuple): - """Evaluate expr, store the value in the "RESULT" register, and return to the caller.""" - expr: "Expr" + """Evaluate expr (if present), store the value in the "RESULT" register, and return to the caller.""" + expr: Optional["Expr"] class Push(NamedTuple): """Used only with Subroutine calls. Acts like Eval, but the destination is the stack.""" @@ -113,7 +155,7 @@ class Push(NamedTuple): class Discard(NamedTuple): """Call a subroutine, then pop the stack, discarding the result.""" - expr: "CallSub" + expr: Union["CallSub", "Invoke"] Stmt = Union[Eval, IndirectWrite, Store, If, While, Push, Discard] @@ -134,10 +176,10 @@ class Local(NamedTuple): name: str # TODO: parameterize, so we can annotate, etc.? class Location(NamedTuple): - """A location identified by segment and index.""" + """A location identified by segment (argument/local) and index.""" kind: VarKind idx: int - name: str # include for debugging purposes + name: str # For debugging class Reg(NamedTuple): """A variable which is local the the subroutine scope, does not need to persist @@ -145,7 +187,17 @@ class Reg(NamedTuple): idx: int name: str # include for debugging purposes -Value = Union[Const, Local, Reg] +class Static(NamedTuple): + """A static variable; just as efficient to access as Reg.""" + name: str + +class Temp(NamedTuple): + """Pseudo-register location used when we know a variable is live only between adjacent + instructions, and it's safe to simply store it in D.""" + name: str # for debugging + + +Value = Union[Const, Local, Reg, Static] """A value that's eligible to be referenced in any statement or expression.""" class Binary(NamedTuple): @@ -161,7 +213,18 @@ class IndirectRead(NamedTuple): """Get the value given an address, aka peek().""" address: "Value" -Expr = Union[CallSub, Const, Local, Location, Reg, Binary, Unary, IndirectRead] +# Extensions: + +class Symbol(NamedTuple): + """Address of an arbitrary symbol.""" + name: str + +class Invoke(NamedTuple): + """Call a subroutine given the address of its entry point.""" + ptr: "Value" + # numArgs? + +Expr = Union[CallSub, Const, Local, Location, Reg, Static, Binary, Unary, IndirectRead, Symbol, Invoke] class Subroutine(NamedTuple): @@ -209,12 +272,14 @@ def next_var(name: Optional[str] = None) -> Local: elif ast.kind == "constructor": this_expr = next_var("this") - def resolve_name(name: str) -> Union[Local, Location]: + def resolve_name(name: str) -> Union[Local, Location, Static]: # TODO: this makes sense for locals, arguments, and statics, but "this" needs to get flattened. # How to deal with that? kind = symbol_table.kind_of(name) if kind == "local": return Local(name) + elif kind == "static": + return Static(name) else: index = symbol_table.index_of(name) return Location(kind, index, name) @@ -227,6 +292,12 @@ def flatten_statement(stmt: jack_ast.Statement) -> List[Stmt]: expr_stmts, expr = flatten_expression(stmt.expr, force=False) let_stmt = Eval(dest=loc, expr=expr) return expr_stmts + [let_stmt] + elif isinstance(loc, Static): + # Note: writing to a static is simple (no need to generate target address), so the + # RHS doesn't need to be in a register. + expr_stmts, expr = flatten_expression(stmt.expr, force=False) + let_stmt = Eval(loc, expr) + return expr_stmts + [let_stmt] elif loc.kind == "this": if isinstance(this_expr, Local): this_var = this_expr @@ -278,7 +349,7 @@ def flatten_statement(stmt: jack_ast.Statement) -> List[Stmt]: stmts, expr = flatten_expression(stmt.expr, force=False) return stmts + [Return(expr)] else: - return [Return(Const(0))] + return [Return(None)] else: raise Exception(f"Unknown statement: {stmt}") @@ -288,35 +359,34 @@ def flatten_condition(expr: jack_ast.Expression) -> Tuple[List[Stmt], Expr, Cmp] preparing a value to be compared with zero. """ - # Collapse simple negated conditions: - if isinstance(expr, jack_ast.UnaryExpression) and expr.op.symbol == "~": - if isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == "<": - expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op(">="), expr.expr.right) - elif isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == ">": - expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op("<="), expr.expr.right) - elif isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == "=": - expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op("!="), expr.expr.right) + # First apply AST-level simplification, which reduces the number of possible shapes: + expr = simplify_expression(expr) # Collapse anything that's become a comparison between two values: if isinstance(expr, jack_ast.BinaryExpression) and expr.op.symbol in ("<", ">", "=", "<=", ">=", "!="): if expr.right == jack_ast.IntegerConstant(0): left_stmts, left_value = flatten_expression(expr.left) return left_stmts, left_value, expr.op.symbol - elif expr.left == jack_ast.IntegerConstant(0): - right_stmts, right_value = flatten_expression(expr.right) - return right_stmts, right_value, negate_cmp(expr.op.symbol) else: left_stmts, left_value = flatten_expression(expr.left) right_stmts, right_value = flatten_expression(expr.right) diff_var = next_var() diff_stmt = Eval(diff_var, Binary(left_value, jack_ast.Op("-"), right_value)) return left_stmts + right_stmts + [diff_stmt], diff_var, expr.op.symbol + elif isinstance(expr, jack_ast.UnaryExpression) and expr.op.symbol == "~": + expr_stmts, expr_value = flatten_expression(expr.expr) + return expr_stmts, expr_value, "=" else: expr_stmts, expr_value = flatten_expression(expr) return expr_stmts, expr_value, "!=" + def negate_cmp(cmp: Cmp) -> Cmp: + """Negate the value.""" return {"<": ">=", ">": "<=", "=": "!=", "<=": ">", ">=": "<", "!=": "="}[cmp] + def invert_cmp(cmp: Cmp) -> Cmp: + """Reverse the operand order.""" + return {"<": ">", ">": "<", "=": "=", "<=": ">=", ">=": "<=", "!=": "!="}[cmp] def flatten_expression(expr: jack_ast.Expression, force=True) -> Tuple[List[Stmt], Expr]: """Reduce an expression to something that's definitely trivial, possibly preceded @@ -327,6 +397,8 @@ def flatten_expression(expr: jack_ast.Expression, force=True) -> Tuple[List[Stmt of Eval or Push. """ + expr = simplify_expression(expr) + if isinstance(expr, jack_ast.IntegerConstant): return [], Const(expr.value) @@ -345,7 +417,7 @@ def flatten_expression(expr: jack_ast.Expression, force=True) -> Tuple[List[Stmt elif isinstance(expr, jack_ast.VarRef): loc = resolve_name(expr.name) - if isinstance(loc, Local): + if isinstance(loc, (Local, Static)): return [], loc # Never wrapped elif loc.kind == "this": if isinstance(this_expr, Local): @@ -394,42 +466,43 @@ def flatten_expression(expr: jack_ast.Expression, force=True) -> Tuple[List[Stmt flat_expr = IndirectRead(address_expr) elif isinstance(expr, jack_ast.SubroutineCall): - pairs = [flatten_expression(a, force=False) for a in expr.args] - arg_stmts = [s for ss, x in pairs for s in ss + [Push(x)]] - if expr.class_name is not None: - stmts = arg_stmts - flat_expr = CallSub(expr.class_name, expr.sub_name, len(expr.args)) - elif expr.var_name is not None: - instance_stmts, instance_expr = flatten_expression(jack_ast.VarRef(expr.var_name), force=False) - stmts = instance_stmts + [Push(instance_expr)] + arg_stmts - target_class = symbol_table.type_of(expr.var_name) - flat_expr = CallSub(target_class, expr.sub_name, len(expr.args) + 1) + if expr.class_name == "Jack" and expr.sub_name == "symbol": + assert len(expr.args) == 1 and isinstance(expr.args[0], jack_ast.StringConstant) + stmts = [] + flat_expr = Symbol(expr.args[0].value) + + elif expr.class_name == "Jack" and expr.sub_name == "invoke": + # TODO: handle arguments, when someone needsw them + assert len(expr.args) == 1 + target_stmts, target_expr = flatten_expression(expr.args[0]) + stmts = target_stmts + flat_expr = Invoke(target_expr) + else: - stmts = [Push(this_expr)] + arg_stmts - target_class = symbol_table.class_name - flat_expr = CallSub(target_class, expr.sub_name, len(expr.args) + 1) + pairs = [flatten_expression(a, force=False) for a in expr.args] + arg_stmts = [s for ss, x in pairs for s in ss + [Push(x)]] + if expr.class_name is not None: + stmts = arg_stmts + flat_expr = CallSub(expr.class_name, expr.sub_name, len(expr.args)) + elif expr.var_name is not None: + instance_stmts, instance_expr = flatten_expression(jack_ast.VarRef(expr.var_name), force=False) + stmts = instance_stmts + [Push(instance_expr)] + arg_stmts + target_class = symbol_table.type_of(expr.var_name) + flat_expr = CallSub(target_class, expr.sub_name, len(expr.args) + 1) + else: + stmts = [Push(this_expr)] + arg_stmts + target_class = symbol_table.class_name + flat_expr = CallSub(target_class, expr.sub_name, len(expr.args) + 1) elif isinstance(expr, jack_ast.BinaryExpression): - # TODO: this transformation is the same as the standard compiler; share that code? - if expr.op.symbol == "*": - return flatten_expression(jack_ast.SubroutineCall("Math", None, "multiply", [expr.left, expr.right]), force=force) - elif expr.op.symbol == "/": - return flatten_expression(jack_ast.SubroutineCall("Math", None, "divide", [expr.left, expr.right]), force=force) - else: - left_stmts, left_expr = flatten_expression(expr.left) - right_stmts, right_expr = flatten_expression(expr.right) - stmts = left_stmts + right_stmts - flat_expr = Binary(left_expr, expr.op, right_expr) + left_stmts, left_expr = flatten_expression(expr.left) + right_stmts, right_expr = flatten_expression(expr.right) + stmts = left_stmts + right_stmts + flat_expr = Binary(left_expr, expr.op, right_expr) elif isinstance(expr, jack_ast.UnaryExpression): stmts, child_expr = flatten_expression(expr.expr) - if isinstance(child_expr, Const) and expr.op.symbol == "-": - # This is ok because this VM handles negative constant values - flat_expr = Const(-child_expr.value) - # elif isinstance(child_expr, Const) and expr.op.symbol == "~": - # TODO: figure out how to evaluate logical negation at compile time, accounting for word size... - else: - flat_expr = Unary(expr.op, child_expr) + flat_expr = Unary(expr.op, child_expr) else: raise Exception(f"Unknown expression: {expr}") @@ -463,6 +536,85 @@ def flatten_expression(expr: jack_ast.Expression, force=True) -> Tuple[List[Stmt return Subroutine(ast.name, num_args, num_vars, statements) +def simplify_expression(expr: jack_ast.Expression) -> jack_ast.BinaryExpression: + """Reduce/canonicalize conditions to a more regular form: + + Replace * and / expressions with calls to multiply/divide. + + For comparison expressions: + - if there's a constant, it's on the right + - if the constant can be re-written to 0, it is + - flatten negated conditions and negative integer constants + """ + + if isinstance(expr, jack_ast.UnaryExpression) and expr.op.symbol == "~": + if isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == "<": + expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op(">="), expr.expr.right) + elif isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == ">": + expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op("<="), expr.expr.right) + elif isinstance(expr.expr, jack_ast.BinaryExpression) and expr.expr.op.symbol == "=": + expr = jack_ast.BinaryExpression(expr.expr.left, jack_ast.Op("!="), expr.expr.right) + + def simplify_constant(x: jack_ast.Expression) -> Optional[int]: + """If possible, reduce the expression to a (possibly negative) constant.""" + if isinstance(x, jack_ast.UnaryExpression) and x.op.symbol == "-" and isinstance(x.expr, jack_ast.IntegerConstant): + return -x.expr.value + elif isinstance(x, jack_ast.IntegerConstant): + return x.value + else: + return None + + if isinstance(expr, jack_ast.BinaryExpression): + left = simplify_expression(expr.left) + right = simplify_expression(expr.right) + + # TODO: this transformation is the same as the standard compiler; share that code? + if expr.op.symbol == "*": + return jack_ast.SubroutineCall("Math", None, "multiply", [left, right]) + elif expr.op.symbol == "/": + return jack_ast.SubroutineCall("Math", None, "divide", [left, right]) + elif expr.op.symbol in ("<", ">", "=", "<=", ">=", "!="): + def invert_cmp(cmp: Cmp) -> Cmp: + """Reverse the operand order.""" + return {"<": ">", ">": "<", "=": "=", "<=": ">=", ">=": "<=", "!=": "!="}[cmp] + + simple_left = simplify_constant(expr.left) + simple_right = simplify_constant(expr.right) + + if simple_left is not None and simple_right is None: + # Constant on the left; reverse the order: + return simplify_expression(jack_ast.BinaryExpression(expr.right, + jack_ast.Op(invert_cmp(expr.op.symbol)), + expr.left)) + elif simple_right is not None: + # Constant on the right; match some common cases: + # The idea is to compare with 0 whenever possible, because that's what the chip + # actually provides directly. + zero = jack_ast.IntegerConstant(0) + if expr.op.symbol == "<" and simple_right == 1: + return jack_ast.BinaryExpression(expr.left, jack_ast.Op("<="), zero) + elif expr.op.symbol == ">=" and simple_right == 1: + return jack_ast.BinaryExpression(expr.left, jack_ast.Op(">"), zero) + elif expr.op.symbol == ">" and simple_right == -1: + return jack_ast.BinaryExpression(expr.left, jack_ast.Op(">="), zero) + elif expr.op.symbol == "<=" and simple_right == -1: + return jack_ast.BinaryExpression(expr.left, jack_ast.Op("<"), zero) + else: + return jack_ast.BinaryExpression(expr.left, + expr.op, + jack_ast.IntegerConstant(simple_right)) + else: + return jack_ast.BinaryExpression(left, expr.op, right) + + elif (isinstance(expr, jack_ast.UnaryExpression) + and expr.op.symbol == "-" + and isinstance(expr.expr, jack_ast.IntegerConstant)): + return jack_ast.IntegerConstant(-expr.expr.value) + + # Nothing matched: + return expr + + class LiveStmt(NamedTuple): statement: Stmt before: Set[str] @@ -540,7 +692,7 @@ def analyze_while(stmt: While, live_at_end) -> Tuple[While, Set[str]]: if len(test_liveness) > 0: live_at_test_start = test_liveness[0].before else: - live_at_test_start = live_at_body_start + live_at_test_start = live_at_test_end stmt = While(test_liveness, stmt.value, stmt.cmp, body_liveness) live = live_at_test_start @@ -584,7 +736,7 @@ def analyze_while(stmt: While, live_at_end) -> Tuple[While, Set[str]]: elif isinstance(stmt, Push): read.update(refs(stmt.expr)) elif isinstance(stmt, Discard): - pass + read.update(refs(stmt.expr)) else: raise Exception(f"Unknown statement: {stmt}") @@ -607,6 +759,8 @@ def refs(expr: Expr) -> Set[str]: return refs(expr.value) elif isinstance(expr, IndirectRead): return refs(expr.address) + elif isinstance(expr, Invoke): + return refs(expr.ptr) else: return set() @@ -620,8 +774,8 @@ def need_saving(liveness: Sequence[LiveStmt]) -> Set[str]: result = set() for l in liveness: - if isinstance(l.statement, (Eval, Push, Discard)) and isinstance(l.statement.expr, CallSub): - result.update(l.before) + if isinstance(l.statement, (Eval, Push, Discard)) and isinstance(l.statement.expr, (CallSub, Invoke)): + result.update(l.during) elif isinstance(l.statement, If): result.update(need_saving(l.statement.when_true)) if l.statement.when_false is not None: @@ -658,7 +812,7 @@ def rewrite_value(value: Value) -> Tuple[Sequence[Stmt], Value]: return [], value def rewrite_expr(expr: Expr) -> Tuple[List[Stmt], Expr]: - if isinstance(expr, (CallSub, Const, Location)): + if isinstance(expr, (CallSub, Const, Location, Static)): return [], expr elif isinstance(expr, Local): if expr in map: @@ -675,6 +829,11 @@ def rewrite_expr(expr: Expr) -> Tuple[List[Stmt], Expr]: elif isinstance(expr, IndirectRead): stmts, address = rewrite_value(expr.address) return stmts, IndirectRead(address) + elif isinstance(expr, Symbol): + return [], expr + elif isinstance(expr, Invoke): + stmts, ptr = rewrite_value(expr.ptr) + return stmts, Invoke(ptr) else: raise Exception(f"Unknown Expr: {expr}") @@ -707,13 +866,17 @@ def rewrite_statement(stmt: Stmt) -> List[Stmt]: body = rewrite_statements(stmt.body) return [While(test + value_stmts, value, stmt.cmp, body)] elif isinstance(stmt, Return): - value_stmts, value = rewrite_expr(stmt.expr) - return value_stmts + [Return(value)] + if stmt.expr is not None: + value_stmts, value = rewrite_expr(stmt.expr) + return value_stmts + [Return(value)] + else: + return [Return(None)] elif isinstance(stmt, Push): expr_stmts, expr = rewrite_expr(stmt.expr) return expr_stmts + [Push(expr)] elif isinstance(stmt, Discard): - return [stmt] + expr_stmts, expr = rewrite_expr(stmt.expr) + return expr_stmts + [Discard(expr)] else: raise Exception(f"Unknown Stmt: {stmt}") @@ -838,24 +1001,24 @@ def visit_stmts(stmts: Optional[Sequence[LiveStmt]]): return color_sets -def lock_down_locals(stmts: Sequence[Stmt], map: Dict[Local, Reg]) -> List[Stmt]: +def resolve_locals(stmts: Sequence[Stmt], map: Dict[Local, V], fail_on_unmapped=True) -> List[Stmt]: """Rewrite statements and expressions, updating references to locals to refer to the given - registers. If any local is not accounted for, fail. + registers. If fail_on_unmapped and any local is not accounted for, fail. """ def rewrite_value(value: Value) -> Value: - if isinstance(value, Local): - # TODO: a more informative error + if value in map: return map[value] + elif fail_on_unmapped and isinstance(value, Local): + raise Exception(f"No assignment for local: {value}") else: return value def rewrite_expr(expr: Expr) -> Expr: - if isinstance(expr, (CallSub, Const, Location)): + if isinstance(expr, (CallSub, Const, Location, Static, Temp)): return expr elif isinstance(expr, Local): - # TODO: a more informative error - return map[expr] + return rewrite_value(expr) elif isinstance(expr, Binary): left_value = rewrite_value(expr.left) right_value = rewrite_value(expr.right) @@ -866,6 +1029,11 @@ def rewrite_expr(expr: Expr) -> Expr: elif isinstance(expr, IndirectRead): address = rewrite_value(expr.address) return IndirectRead(address) + elif isinstance(expr, Symbol): + return expr + elif isinstance(expr, Invoke): + ptr = rewrite_value(expr.ptr) + return Invoke(ptr) else: raise Exception(f"Unknown Expr: {expr}") @@ -879,7 +1047,6 @@ def rewrite_statement(stmt: Stmt) -> Stmt: value = rewrite_value(stmt.value) return IndirectWrite(address, value) elif isinstance(stmt, Store): - # print(f"rewrite Store: {stmt}") value = rewrite_value(stmt.value) return Store(stmt.location, value) elif isinstance(stmt, If): @@ -893,13 +1060,14 @@ def rewrite_statement(stmt: Stmt) -> Stmt: body = rewrite_statements(stmt.body) return While(test, value, stmt.cmp, body) elif isinstance(stmt, Return): - value = rewrite_expr(stmt.expr) + value = rewrite_expr(stmt.expr) if stmt.expr is not None else None return Return(value) elif isinstance(stmt, Push): expr = rewrite_expr(stmt.expr) return Push(expr) elif isinstance(stmt, Discard): - return stmt + expr = rewrite_expr(stmt.expr) + return Discard(expr) else: raise Exception(f"Unknown Stmt: {stmt}") @@ -912,6 +1080,141 @@ def rewrite_statements(stmts: Optional[Sequence[Stmt]]) -> Optional[List[Stmt]]: return rewrite_statements(stmts) +def find_transients(stmts: Sequence[LiveStmt]) -> Set[str]: + """Identify Locals that exist only to carry a result between a pair of adjacent statements, + where the code generator is able to keep the value in D: + - live only in one statement (i.e. assigned in one, then used in the next) + - the statement that uses it doesn't need to overwrite D + """ + + counts = Counter() + + def count_stmt(s: LiveStmt): + for l in s.before: counts[l] += 1 + if isinstance(s.statement, If): + for cs in s.statement.when_true: count_stmt(cs) + for cs in s.statement.when_false or []: count_stmt(cs) + elif isinstance(s.statement, While): + for cs in s.statement.test: count_stmt(cs) + for cs in s.statement.body: count_stmt(cs) + # Tricky: the value's refs aren't represented in LiveStmt + for l in refs(s.statement.value): + counts[l] += 1 + + for s in stmts: count_stmt(s) + + # Candidate locals: live only in a single statement, the one where they're used + one_time = { l for (l, x) in counts.items() if x == 1 } + + # Try re-writing the statement where the var is used: + def visit_stmts(l: Local, stmts: Sequence[LiveStmt]) -> bool: + return any (visit_stmt(l, s) for s in stmts) + + def visit_stmt(l: Local, stmt: LiveStmt) -> bool: + if isinstance(stmt.statement, If): + if l in refs(stmt.statement.value): return True + elif any(visit_stmt(l, s) for s in stmt.statement.when_true): return True + elif (stmt.statement.when_false is not None + and any(visit_stmt(l, s) for s in stmt.statement.when_false)): return True + elif isinstance(stmt.statement, While): + if l in refs(stmt.statement.value): return True + elif any(visit_stmt(l, s) for s in stmt.statement.test): return True + elif any(visit_stmt(l, s) for s in stmt.statement.body): return True + elif l in stmt.before: + # Tricky: this little hack fails if the stmt is nested, so we just don't do this path + # if it's If/While + rewritten, = resolve_locals([stmt.statement], { Local(l): Temp(l) }, fail_on_unmapped=False) + return is_translatable(rewritten) + else: + return False + + # FIXME: this is patently O(n^2). Fix that by constructing a map of one-time locals to the + # statements that use them, and then only inspect those. + return { l for l in one_time if visit_stmts(l, stmts) } + + +def is_translatable(stmt: Stmt) -> bool: + """True if the statement can be translated to assembly without disturbing Temp values stored in D. + The statement may contain unresolved Locals; they are assumed to represent Reg locations that + will be assigned later. + + Note: If/While aren't handled here, because a) their test values are always transient + """ + + def translatable_expr(expr: Expr) -> bool: + if isinstance(expr, CallSub): + raise Exception("silly question: a CallSub can't refer to a one-time (or any) local") + elif isinstance(expr, Const): + # A constant should never in and of itself require D to be overwritten. + return True + elif isinstance(expr, (Local, Reg, Static)): + # These are all places values can be accessed from without touching D + return True + elif isinstance(expr, Temp): + # The temp location itself is trivially ok + return True + elif isinstance(expr, Location): + # Accessing a stack-allocated variable may involve overwriting D + return False + elif isinstance(expr, Binary): + # Tricky: a binary expression can take one or both of its arguments from D, in some cases. + if isinstance(expr.left, Temp) and isinstance(expr.right, Temp): + return True + elif isinstance(expr.left, Temp) and a_only(expr.right): + return True + elif a_only(expr.left) and isinstance(expr.right, Temp): + return True + else: + # print(f"unsafe: {_Expr_str(expr)}") + # return False + raise Exception("Doesn't happen?!") + elif isinstance(expr, Unary): + # *Not* and *negate* can be done in place + return translatable_expr(expr.value) + elif isinstance(expr, IndirectRead): + # The *read* isn't a problem + return translatable_expr(expr.address) + elif isinstance(expr, Symbol): + return True + elif isinstance(expr, Invoke): + # Need D to set up the return address + return False + else: + raise Exception(f"Unexpected expr: {expr}") + + def a_only(expr: Expr) -> bool: + """True if the value of the expression can be constructed directly in A (see value_to_a).""" + return isinstance(expr, (Const, Reg, Local, Static)) + + if isinstance(stmt, Eval): + return translatable_expr(stmt.expr) + elif isinstance(stmt, IndirectWrite): + # The *value* can come from D, if the *address* doesn't need to touch it: + if a_only(stmt.address) and translatable_expr(stmt.value): + return True + # Or, the *address* can come from D, if the value can be constructed by the ALU: + elif translatable_expr(stmt.address) and isinstance(stmt.value, Const) and (-1 <= stmt.value.value <= 1): + return True + else: + return False + elif isinstance(stmt, Store): + # TODO: this could be ok if the code generator can construct the address without D, + # which it can, if the index is 0 or 1 (or even -1), or even some larger values + # with some additional cycles if it's till cheaper than copying to a Register and + # back (which is 4 instructions). + return False + elif isinstance(stmt, (If, While)): + raise Exception("Not handled here") + elif isinstance(stmt, Return): + return stmt.expr is None or translatable_expr(stmt.expr) + elif isinstance(stmt, Push): + return translatable_expr(stmt.expr) + elif isinstance(stmt, Discard): + return translatable_expr(stmt.expr) + else: + raise Exception(f"Unexpected stmt: {stmt}") + + def phase_two(ast: Subroutine, reg_count: int = 8) -> Subroutine: """The input is IR with all locals represented by Local. @@ -922,6 +1225,11 @@ def phase_two(ast: Subroutine, reg_count: int = 8) -> Subroutine: # TODO: treat THIS and THAT as additional locations for "saved" vars. # TODO: color vars needing saving as well? probably not worth it for stack-allocated. + # + # Identify locals that need to be stored on the stack (because their values + # need to persist across subroutine calls): + # + promoted_count = 0 def next_location(var: Local) -> Location: nonlocal promoted_count @@ -933,45 +1241,63 @@ def next_location(var: Local) -> Location: need_promotion = need_saving(liveness) - # for s in liveness: - # print(_Stmt_str(s)) - # print(f"need saving: {need_promotion}") + if PRINT_LIVENESS: + print(ast._replace(body=liveness)) + print(f"need saving: [{', '.join(str(n) for n in need_promotion)}]") + print() body = promote_locals(ast.body, { Local(l): next_location(Local(l)) for l in need_promotion }, "p_") - while True: - liveness2 = analyze_liveness(body) - need_promotion2 = need_saving(liveness2) + liveness2 = analyze_liveness(body) + need_promotion2 = need_saving(liveness2) - # Sanity check: additional promotion - if len(need_promotion2) > 0: - # for s in liveness2: - # print(_Stmt_str(s)) - # print(f"need saving after one round: {need_promotion2}") - raise Exception(f"More than one round of promotion needed. Need promotion: {need_promotion2}; in {ast.name}()") + if PRINT_LIVENESS: + print(ast._replace(body=liveness2)) - local_sets = color_locals(liveness2) + # Sanity check: additional promotion needed + if len(need_promotion2) > 0: + raise Exception(f"One-pass promotion inadequate Need promotion: {need_promotion2}; in {ast.name}()") - if len(local_sets) > reg_count: - unallocatable_sets = local_sets[reg_count:] - print(f"Unable to fit local variables in {reg_count} registers; no space for {[ {l.name for l in s} for s in unallocatable_sets]}") + # + # Identify temps that can be passed in D for zero overhead: + # - body = promote_locals(body, { l: next_location(l) for s in unallocatable_sets for l in s }, "q_") + transients = find_transients(liveness2) + body = resolve_locals(body, {Local(l): Temp(l) for l in transients}, fail_on_unmapped=False) + liveness3 = analyze_liveness(body) # TODO: is it safe to re-analyze? - else: - reg_map = { - l: Reg(idx=i, name=l.name) - for i, ls in enumerate(local_sets) - for l in ls - } + if PRINT_LIVENESS: + print(ast._replace(body=liveness3)) + print() - reg_pressure = 0 if reg_map == {} else max(r.idx+1 for r in reg_map.values()) - # print(f"Registers allocated in {ast.name}: {reg_pressure} ({100*reg_pressure/reg_count:.1f}%)") + # + # Anything left now can be stored in a register, and they better all fit: + # - reg_body = lock_down_locals(body, reg_map) + local_sets = color_locals(liveness3) - return Subroutine(ast.name, ast.num_args, promoted_count, reg_body) + if len(local_sets) > reg_count: + unallocatable_sets = local_sets[reg_count:] + + print(f"Unable to fit local variables in {reg_count} registers; no space for {[ {l.name for l in s} for s in unallocatable_sets]}") + + raise Exception("One-pass promotion failed!?") + # body = promote_locals(body, { l: next_location(l) for s in unallocatable_sets for l in s }, "q_") + + else: + reg_map = { + l: Reg(idx=i, name=l.name) + for i, ls in enumerate(local_sets) + for l in ls + } + + reg_pressure = 0 if reg_map == {} else max(r.idx+1 for r in reg_map.values()) + # print(f"Registers allocated in {ast.name}: {reg_pressure} ({100*reg_pressure/reg_count:.1f}%)") + + reg_body = resolve_locals(body, reg_map, fail_on_unmapped=True) + + return Subroutine(ast.name, ast.num_args, promoted_count, reg_body) def compile_class(ast: jack_ast.Class) -> Class: @@ -1000,10 +1326,15 @@ def compile_class(ast: jack_ast.Class) -> Class: RESULT = "R12" # for now, just use one of the registers also used for local variables. class Translator(solved_07.Translator): - def __init__(self): - self.asm = AssemblySource() + def __init__(self, asm=None): + self.asm = asm if asm else AssemblySource() solved_07.Translator.__init__(self, self.asm) + # HACK: stash the leaf-ishness of the current subroutine statefully, so we don't have to + # thread it through the traversal. When we're dealing with a leaf, this is the number of + # args. Otherwise, None. + self.leaf_sub_args = None + # self.preamble() # called by the loader, apparently def handle(self, op): @@ -1015,7 +1346,8 @@ def translate_class(self, class_ast: Class): self.translate_subroutine(s, class_ast.name) def translate_subroutine(self, subroutine_ast: Subroutine, class_name: str): - # print(subroutine_ast) + if PRINT_IR: + print(subroutine_ast); print() # if self.last_function_start is not None: # instrs = self.asm.instruction_count - self.last_function_start @@ -1032,60 +1364,121 @@ def translate_subroutine(self, subroutine_ast: Subroutine, class_name: str): self.asm.start(f"function {class_name}.{subroutine_ast.name} {subroutine_ast.num_vars}") self.asm.label(f"{self.function_namespace}") - # Note: this could be done with a common sequence in the preamble, but jumping there and - # back would cost at least 6 cycles or so, and the goal here is to get small by generating - # tighter code, not save space in ROM by adding runtime overhead. - # TODO: avoid this overhead entirely for leaf functions, by just not adjusting the stack - # at all. - self.asm.comment("push the return address, then LCL and ARG") - self.asm.instr(f"@{RETURN_ADDRESS}") - self.asm.instr("D=M") - self._push_d() - self.asm.instr("@LCL") - self.asm.instr("D=M") - self._push_d() - self.asm.instr("@ARG") - self.asm.instr("D=M") - self._push_d() + def uses_stack(stmt: Stmt): + if isinstance(stmt, Eval): + return isinstance(stmt.expr, (CallSub, Invoke)) + elif isinstance(stmt, IndirectWrite): + return False + elif isinstance(stmt, Store): + return False + elif isinstance(stmt, If): + return any(uses_stack(s) for s in stmt.when_true + (stmt.when_false or [])) + elif isinstance(stmt, While): + return any(uses_stack(s) for s in stmt.test + stmt.body) + elif isinstance(stmt, Return): + return isinstance(stmt.expr, (CallSub, Invoke)) # FIXME: does this happen? + elif isinstance(stmt, Push): + return True + elif isinstance(stmt, Discard): + return isinstance(stmt.expr, (CallSub, Invoke)) + else: + raise Exception(f"Unknown Stmt: {stmt}") + + is_leaf = OPTIMIZE_LEAF_FUNCTIONS and not any(uses_stack(s) for s in subroutine_ast.body) + + if is_leaf: + # TODO: if solved_07.INITIALIZE_LOCALS? + if subroutine_ast.num_vars > 0: + # Note: this is probably pretty rare. You need to have a leaf function with more + # live variables than there are registers (typically 8). + self.asm.comment(f"initialize locals ({subroutine_ast.num_vars})") + self.asm.instr("@SP") + self.asm.instr("M=0") + for _ in range(subroutine_ast.num_vars-1): + self.asm.instr("A=A+1") + self.asm.instr("M=0") + else: + self.asm.comment("no locals to initialize (leaf function), so no-op") + # Bogus: so the function label and the first stmt don't end up at the same address + self.asm.instr("D=D") - self.asm.comment("LCL = SP") - self.asm.instr("@SP") - self.asm.instr("D=M") - self.asm.instr("@LCL") - self.asm.instr("M=D") + else: + # Note: this could be done with a common sequence in the preamble, but jumping there and + # back would cost at least 6 cycles or so, and the goal here is to get small by generating + # tighter code, not save space in ROM by adding runtime overhead. + # TODO: avoid this overhead entirely for leaf functions, by just not adjusting the stack + # at all. + self.asm.comment("push the return address, then LCL and ARG") + self.asm.instr(f"@{RETURN_ADDRESS}") + self.asm.instr("D=M") + self._push_d() + self.asm.instr("@LCL") + self.asm.instr("D=M") + self._push_d() + self.asm.instr("@ARG") + self.asm.instr("D=M") + self._push_d() - self.asm.comment("ARG = SP - (num_args + 3)") - self.asm.instr(f"@{subroutine_ast.num_args + 3}") - self.asm.instr("D=A") - self.asm.instr("@SP") - self.asm.instr("D=M-D") - self.asm.instr("@ARG") - self.asm.instr("M=D") + self.asm.comment("LCL = SP") + self.asm.instr("@SP") + self.asm.instr("D=M") + self.asm.instr("@LCL") + self.asm.instr("M=D") - if subroutine_ast.num_vars > 0: - self.asm.comment(f"space for locals ({subroutine_ast.num_vars})") - self.reserve_local_space(subroutine_ast.num_vars) + self.asm.comment("ARG = SP - (num_args + 3)") + self.asm.instr(f"@{subroutine_ast.num_args + 3}") + self.asm.instr("D=A") + self.asm.instr("@SP") + self.asm.instr("D=M-D") + self.asm.instr("@ARG") + self.asm.instr("M=D") + + if subroutine_ast.num_vars > 0: + self.asm.comment(f"space for locals ({subroutine_ast.num_vars})") + self.reserve_local_space(subroutine_ast.num_vars) # Now the body: + if is_leaf: + self.leaf_sub_args = subroutine_ast.num_args + else: + self.leaf_sub_args = None + for s in subroutine_ast.body: self._handle(s) + self.asm.blank() instr_count_after = self.asm.instruction_count - # print(f"Translated {class_name}.{subroutine_ast.name}; instructions: {instr_count_after - instr_count_before:,d}") + # print(f"Translated {class_name}.{subroutine_ast.name}; instructions: {instr_count_after - instr_count_before:,d}\n") # DEBUG # Statements: def handle_Eval(self, ast: Eval): - assert isinstance(ast.dest, Reg) + assert isinstance(ast.dest, (Reg, Static, Temp)) if not isinstance(ast.expr, CallSub): self.asm.start(f"eval-{self.describe_expr(ast.expr)} {_Stmt_str(ast)}") + # Easy case: leave the result in D for the next statement to pick up + if isinstance(ast.dest, Temp): + self._handle(ast.expr) + return + + symbol_name = self.symbol(ast.dest) + # Do the update in-place if possible: - if isinstance(ast.expr, Binary) and isinstance(ast.expr.left, Reg) and ast.dest.idx == ast.expr.left.idx: + if (isinstance(ast.expr, Binary) + and symbol_name == self.symbol(ast.expr.left) and symbol_name == self.symbol(ast.expr.right)): + # This pretty much only handles x = x + x, but that occurs the in hot loop in multiply + op = self.binary_op_alu(ast.expr.op) + if op is not None: + self.asm.instr(f"@{symbol_name}") + self.asm.instr(f"D=M") + self.asm.instr(f"M=M{op}D") + return + elif isinstance(ast.expr, Binary) and symbol_name == self.symbol(ast.expr.left): op = self.binary_op_alu(ast.expr.op) if op is not None: right_imm = self.immediate(ast.expr.right) @@ -1093,34 +1486,36 @@ def handle_Eval(self, ast: Eval): if right_imm == 0: pass # nothing to do: rX = rX + 0 else: - self.asm.instr(f"@R{5+ast.dest.idx}") + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M=M{right_imm:+}") elif op == "-" and right_imm is not None: if right_imm == 0: pass # nothing to do: rX = rX - 0 else: - self.asm.instr(f"@R{5+ast.dest.idx}") + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M=M{-right_imm:+}") else: - self._handle(ast.expr.right) # D = right - self.asm.instr(f"@R{5+ast.dest.idx}") + if not isinstance(ast.expr.right, Temp): + self._handle(ast.expr.right) # D = right + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M=M{op}D") return - elif isinstance(ast.expr, Binary) and isinstance(ast.expr.right, Reg) and ast.dest.idx == ast.expr.right.idx: + elif isinstance(ast.expr, Binary) and symbol_name == self.symbol(ast.expr.right): op = self.binary_op_alu(ast.expr.op) if op is not None: - self._handle(ast.expr.left) # D = left - self.asm.instr(f"@R{5+ast.dest.idx}") + if not isinstance(ast.expr.left, Temp): + self._handle(ast.expr.left) # D = left + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M=D{op}M") return - elif isinstance(ast.expr, Unary) and ast.dest == ast.expr: - self.asm.instr(f"@R{5+ast.dest.idx}") + elif isinstance(ast.expr, Unary) and symbol_name == self.symbol(ast.expr.value): + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M={self.unary_op(ast.expr.op)}M") return imm = self.immediate(ast.expr) if imm is not None: - self.asm.instr(f"@R{5+ast.dest.idx}") + self.asm.instr(f"@{symbol_name}") self.asm.instr(f"M={imm}") else: self._handle(ast.expr) @@ -1128,7 +1523,7 @@ def handle_Eval(self, ast: Eval): if isinstance(ast.expr, CallSub): self.asm.start(f"eval-result {_Expr_str(ast.dest)} = ") - self.asm.instr(f"@R{5+ast.dest.idx}") + self.asm.instr(f"@{symbol_name}") self.asm.instr("M=D") def handle_IndirectWrite(self, ast: IndirectWrite): @@ -1136,8 +1531,16 @@ def handle_IndirectWrite(self, ast: IndirectWrite): imm = self.immediate(ast.value) if imm is not None: - self.value_to_a(ast.address) + if isinstance(ast.address, Temp): + # TODO: this is a little silly; the previous instruction could probably just + # load the value into A if we knew that's where it would be useful + self.asm.instr("A=D") + else: + self.value_to_a(ast.address) self.asm.instr(f"M={imm}") + elif isinstance(ast.value, Temp): + self.value_to_a(ast.address) + self.asm.instr("M=D") else: self._handle(ast.value) self.value_to_a(ast.address) @@ -1148,19 +1551,16 @@ def handle_Store(self, ast: Store): imm = self.immediate(ast.value) - kind, index = ast.location.kind, ast.location.idx - if kind == "static": - if imm is not None: - self.asm.instr(f"@{self.class_namespace}.static{index}") - self.asm.instr(f"M={imm}") + kind = ast.location.kind + if self.leaf_sub_args is not None: + # LCL and ARG are not used; just index off of SP (below for args, above for locals) + segment_ptr = "SP" + if kind == "argument": + index = -self.leaf_sub_args + ast.location.idx + elif kind == "local": + index = ast.location.idx else: - self._handle(ast.value) - self.asm.instr(f"@{self.class_namespace}.static{index}") - self.asm.instr("M=D") - - elif kind == "field": - raise Exception(f"should have been rewritten: {ast}") - + raise Exception(f"Unknown location: {ast}") else: if kind == "argument": segment_ptr = "ARG" @@ -1168,51 +1568,66 @@ def handle_Store(self, ast: Store): segment_ptr = "LCL" else: raise Exception(f"Unknown location: {ast}") + index = ast.location.idx - # Super common case: initialize a var to 0 or 1 (or -1): - if imm is not None: + # Super common case: initialize a var to 0 or 1 (or -1): + if imm is not None: + if index == 0: + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M") + self.asm.instr(f"M={imm}") + elif index == 1: + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M+1") + self.asm.instr(f"M={imm}") + elif index == -1: + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M-1") + self.asm.instr(f"M={imm}") + elif index > 0: + self.asm.instr(f"@{index}") + self.asm.instr("D=A") + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M+D") + self.asm.instr(f"M={imm}") + else: + self.asm.instr(f"@{-index}") + self.asm.instr("D=A") + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M-D") + self.asm.instr(f"M={imm}") + + else: + # For small index, compute the destination address without clobbering D: + # Note: not optimizing negative indexes because we rarely see store to argument + if 0 <= index <= 6: + self._handle(ast.value) + self.asm.instr(f"@{segment_ptr}") if index == 0: - self.asm.instr(f"@{segment_ptr}") self.asm.instr("A=M") - self.asm.instr(f"M={imm}") - elif index == 1: - self.asm.instr(f"@{segment_ptr}") - self.asm.instr("A=M+1") - self.asm.instr(f"M={imm}") else: - self.asm.instr(f"@{index}") - self.asm.instr("D=A") - self.asm.instr(f"@{segment_ptr}") - self.asm.instr("A=M+D") - self.asm.instr(f"M={imm}") + self.asm.instr("A=M+1") + for _ in range(index-1): + self.asm.instr("A=A+1") + self.asm.instr("M=D") else: - if index <= 6: - self._handle(ast.value) - self.asm.instr(f"@{segment_ptr}") - if index == 0: - self.asm.instr("A=M") - else: - self.asm.instr("A=M+1") - for _ in range(index-1): - self.asm.instr("A=A+1") - self.asm.instr("M=D") - - else: + if index > 0: self.asm.instr(f"@{index}") self.asm.instr("D=A") - self.asm.instr(f"@{segment_ptr}") - self.asm.instr("D=M+D") - self.asm.instr("@R15") # code smell: needing R15 shows that this isn't actually atomic - self.asm.instr("M=D") - self._handle(ast.value) - self.asm.instr("@R15") - self.asm.instr("A=M") - self.asm.instr("M=D") + else: + self.asm.instr(f"@{-index}") + self.asm.instr("D=-A") + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("D=M+D") + self.asm.instr("@R15") # code smell: needing R15 shows that this isn't actually atomic + self.asm.instr("M=D") + self._handle(ast.value) + self.asm.instr("@R15") + self.asm.instr("A=M") + self.asm.instr("M=D") def handle_If(self, ast: If): - self.asm.comment("if...") - if ast.when_false is None: # Awesome: when there's no else, and the condition is simple, it turns into a single branch. # TODO: to avoid constructing boolean values, probably want to put left _and_ right values @@ -1221,7 +1636,8 @@ def handle_If(self, ast: If): end_label = self.asm.next_label("end") self.asm.start(f"if {_Expr_str(ast.value)} {ast.cmp} 0?") - self._handle(ast.value) + if not isinstance(ast.value, Temp): + self._handle(ast.value) self.asm.instr(f"@{end_label}") self.asm.instr(f"D;J{self.compare_op_neg(ast.cmp)}") @@ -1235,15 +1651,17 @@ def handle_If(self, ast: If): false_label = self.asm.next_label("false") self.asm.start(f"if/else {_Expr_str(ast.value)} {ast.cmp} 0?") - self._handle(ast.value) + if not isinstance(ast.value, Temp): + self._handle(ast.value) self.asm.instr(f"@{false_label}") self.asm.instr(f"D;J{self.compare_op_neg(ast.cmp)}") for s in ast.when_true: self._handle(s) - self.asm.instr(f"@{end_label}") - self.asm.instr(f"0;JMP") + if not isinstance(ast.when_true[-1], Return): + self.asm.instr(f"@{end_label}") + self.asm.instr(f"0;JMP") self.asm.label(false_label) for s in ast.when_false: @@ -1272,7 +1690,8 @@ def handle_While(self, ast): self._handle(s) self.asm.start(f"while-test {_Expr_str(ast.value)} {ast.cmp} 0?") - self._handle(ast.value) + if not isinstance(ast.value, Temp): + self._handle(ast.value) self.asm.instr(f"@{body_label}") self.asm.instr(f"D;J{self.compare_op_pos(ast.cmp)}") @@ -1281,7 +1700,7 @@ def handle_Return(self, ast: Return): self.handle_CallSub(ast.expr) self.asm.comment(f"leave the result in {RESULT}") - else: + elif ast.expr is not None: self.asm.start(f"eval-{self.describe_expr(ast.expr)} {_Expr_str(ast.expr)} (for return)") # Save a cycle for "return 0": @@ -1294,7 +1713,30 @@ def handle_Return(self, ast: Return): self.asm.instr(f"@{RESULT}") self.asm.instr("M=D") - self.return_op() + if self.leaf_sub_args is not None: + self.asm.start("return (from leaf)") + if self.leaf_sub_args > 0: + if self.leaf_sub_args == 1: + self.asm.comment("pop 1 argument") + self.asm.instr("@SP") + self.asm.instr("M=M-1") + elif self.leaf_sub_args == 2: + self.asm.comment("pop 2 arguments") + self.asm.instr("@SP") + self.asm.instr("M=M-1") + self.asm.instr("M=M-1") + else: + self.asm.comment("pop arguments") + self.asm.instr(f"@{self.leaf_sub_args}") + self.asm.instr("D=A") + self.asm.instr("@SP") + self.asm.instr("M=M-D") + self.asm.instr(f"@{RETURN_ADDRESS}") + self.asm.instr("A=M") + self.asm.instr("0;JMP") + + else: + self.return_op() def handle_Push(self, ast): if not isinstance(ast.expr, CallSub): @@ -1308,7 +1750,8 @@ def handle_Push(self, ast): self.asm.instr("A=M-1") self.asm.instr(f"M={imm}") else: - self._handle(ast.expr) + if not isinstance(ast.expr, Temp): + self._handle(ast.expr) if isinstance(ast.expr, CallSub): self.asm.start(f"push-result {_Expr_str(ast.expr)}") @@ -1321,10 +1764,17 @@ def handle_Push(self, ast): def handle_Discard(self, ast: Discard): # Note: now that results are passed in a register, there's no cleanup to do when # the result is not used. - self.call(ast.expr) + + if isinstance(ast.expr, CallSub): + self.call(ast.expr) + elif isinstance(ast.expr, Invoke): + self.invoke(ast.expr) + else: + raise Exception(f"Unexpected expr; {_Stmt_str(ast)}") self.asm.comment(f"ignore the result") + def compare_op_neg(self, cmp: Cmp): return { "=": "NE", @@ -1372,6 +1822,22 @@ def call(self, ast: CallSub): self.asm.label(return_label) + def invoke(self, ast: Invoke): + return_label = self.asm.next_label("return_address") + + self.asm.start(f"call (* {_Expr_str(ast.ptr)})") + + self.asm.instr(f"@{return_label}") + self.asm.instr("D=A") + self.asm.instr(f"@{RETURN_ADDRESS}") + self.asm.instr("M=D") + + self.value_to_a(ast.ptr) + self.asm.instr("0;JMP") + + self.asm.label(return_label) + + def handle_Const(self, ast: Const): if -1 <= ast.value <= 1: @@ -1385,18 +1851,25 @@ def handle_Const(self, ast: Const): def handle_Location(self, ast: Location): kind, index = ast.kind, ast.idx - if ast.kind == "static": - self.asm.instr(f"@{self.class_namespace}.static{ast.idx}") - self.asm.instr("D=M") - elif ast.kind == "field": + if ast.kind in ("field", "static"): raise Exception(f"should have been rewritten: {ast}") else: - if ast.kind == "argument": - segment_ptr = "ARG" - elif ast.kind == "local": - segment_ptr = "LCL" + if self.leaf_sub_args is not None: + # LCL and ARG are not used; just index off of SP (below for args, above for locals) + segment_ptr = "SP" + if ast.kind == "argument": + index = -self.leaf_sub_args + ast.idx + elif ast.kind == "local": + index = ast.index + else: + raise Exception(f"Unknown location: {ast}") else: - raise Exception(f"Unknown location: {ast}") + if ast.kind == "argument": + segment_ptr = "ARG" + elif ast.kind == "local": + segment_ptr = "LCL" + else: + raise Exception(f"Unknown location: {ast}") if index == 0: self.asm.instr(f"@{segment_ptr}") @@ -1408,36 +1881,73 @@ def handle_Location(self, ast: Location): self.asm.instr(f"@{segment_ptr}") self.asm.instr("A=M+1") self.asm.instr("A=A+1") - else: + elif index > 0: self.asm.instr(f"@{index}") self.asm.instr("D=A") self.asm.instr(f"@{segment_ptr}") - self.asm.instr("A=D+M") + self.asm.instr("A=M+D") + elif index == -1: + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M-1") + elif index == -2: + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M-1") + self.asm.instr("A=A-1") + else: + self.asm.instr(f"@{-index}") + self.asm.instr("D=A") + self.asm.instr(f"@{segment_ptr}") + self.asm.instr("A=M-D") self.asm.instr("D=M") + def handle_Reg(self, ast: Reg): self.asm.instr(f"@R{5+ast.idx}") self.asm.instr("D=M") + + def handle_Static(self, ast: Static): + symbol_name = f"{self.class_namespace}.static_{ast.name}" + self.asm.instr(f"@{symbol_name}") + self.asm.instr("D=M") + + def handle_Temp(self, ast: Temp): + raise Exception("Unsafe! Callers should explicitly handle Temp when they can do so safely.") + def handle_Binary(self, ast: Binary): - left_imm = self.immediate(ast.left) + left_symbol = self.symbol(ast.left) alu_op = self.binary_op_alu(ast.op) right_imm = self.immediate(ast.right) - if alu_op == "+" and isinstance(ast.left, Reg) and right_imm is not None: + if alu_op == "+" and left_symbol is not None and right_imm is not None: # e.g. r0 + 1 -> @R5; D=M+1 - self.asm.instr(f"@R{5+ast.left.idx}") + self.asm.instr(f"@{left_symbol}") self.asm.instr(f"D=M{right_imm:+}") return - elif alu_op == "-" and isinstance(ast.left, Reg) and right_imm is not None: + elif alu_op == "-" and left_symbol is not None and right_imm is not None: # e.g. r0 - 1 -> @R5; D=M-1 - self.asm.instr(f"@R{5+ast.left.idx}") + self.asm.instr(f"@{left_symbol}") self.asm.instr(f"D=M{-right_imm:+}") return elif alu_op is not None: - self._handle(ast.left) # D = left - self.value_to_a(ast.right) # A = right - self.asm.instr(f"D=D{alu_op}A") + left_in_d = isinstance(ast.left, Temp) + right_in_d = isinstance(ast.right, Temp) + if left_in_d and right_in_d: + # e.g. x = x + x + self.asm.instr("A=D") + self.asm.instr(f"D=D{alu_op}A") + elif left_in_d: + # D = left (from previous instr) + self.value_to_a(ast.right) # A = right + self.asm.instr(f"D=D{alu_op}A") + elif right_in_d: + self.value_to_a(ast.left) # A = left + # D = right (from previous instr) + self.asm.instr(f"D=A{alu_op}D") # Note reversed operands (for -) + else: + self._handle(ast.left) # D = left + self.value_to_a(ast.right) # A = right + self.asm.instr(f"D=D{alu_op}A") return cmp_op = self.binary_op_cmp(ast.op) @@ -1445,7 +1955,7 @@ def handle_Binary(self, ast: Binary): # Massive savings here for this compiler; by pushing the comparison all the way into # the ALU, this is one branch, with a specific condition code, to pick the right result. # But having to leave the result in D kind of spoils the party; for one thing, have to - # branch again to skip the no-taken branch, which would have written the other result. + # branch again to skip the not-taken branch, which would have written the other result. # TODO: this is almost certainly wrong for signed values where the difference overflows, though: # -30,000 > 30,000 @@ -1457,15 +1967,35 @@ def handle_Binary(self, ast: Binary): # Note: if the right operand is -1,0,1, we shave a few cycles. An earlier phase should # take care of rewriting conditions into that form where possible. - self._handle(ast.left) # D = left - if right_imm == 0: - pass # comparing with zero, so D already has left - 0, effectively - elif right_imm is not None and -1 <= right_imm <= 1: - self.asm.instr(f"D=D{-right_imm:+d}") # D = left - right (so, positive if left > right) + # Now it breaks down into 6 possible cases, depending on whether there's an immediate + # on the right, and whether either operand is in D: + if right_imm is not None: + if isinstance(ast.left, Temp): + pass # D = left (from previous instr) + else: + self._handle(ast.left) # D = left + + if right_imm == 0: + pass # D = left - 0 (with no additional work) + else: + self.asm.instr(f"D=D{-right_imm:+d}") # D = left - right (so, positive if left > right) + + elif isinstance(ast.left, Temp): + # D = left (from previous instr) + self.value_to_a(ast.right) # A = right + self.asm.instr("D=D-A") # D = left - right (so, positive if left > right) + + elif isinstance(ast.right, Temp): + # D = right (from previous instr) + self.value_to_a(ast.left) # A = left + self.asm.instr("D=A-D") # D = left - right (so, positive if left > right) + else: + self._handle(ast.left) # D = left self.value_to_a(ast.right) # A = right self.asm.instr("D=D-A") # D = left - right (so, positive if left > right) + self.asm.instr(f"@{true_label}") self.asm.instr(f"D;J{cmp_op}") @@ -1478,7 +2008,7 @@ def handle_Binary(self, ast: Binary): self.asm.label(end_label) return - raise Exception(f"TODO: {ast}") + raise Exception(f"Unexpected expr: {_Expr_str(ast)}") def binary_op_alu(self, op: jack_ast.Op) -> Optional[str]: return { @@ -1493,6 +2023,9 @@ def binary_op_cmp(self, op: jack_ast.Op) -> Optional[str]: "<": "LT", ">": "GT", "=": "EQ", + "<=": "LE", + ">=": "GE", + "!=": "NE", }.get(op.symbol) def handle_Unary(self, ast: Unary): @@ -1501,16 +2034,26 @@ def handle_Unary(self, ast: Unary): self.asm.instr(f"D={self.unary_op(ast.op)}M") else: # Note: ~(Const) isn't being evaluated in the compiler yet, but should be. - self._handle(ast.value) + if not isinstance(ast.value, Temp): + self._handle(ast.value) self.asm.instr(f"D={self.unary_op(ast.op)}D") def unary_op(self, op: jack_ast.Op) -> str: return {"-": "-", "~": "!"}[op.symbol] def handle_IndirectRead(self, ast: IndirectRead): - self.value_to_a(ast.address) + if isinstance(ast.address, Temp): + # TODO: this is a little silly; the previous instruction could probably just + # load the value into A if we knew that's where it would be useful + self.asm.instr("A=D") + else: + self.value_to_a(ast.address) self.asm.instr("D=M") + def handle_Symbol(self, ast: Symbol): + self.asm.instr(f"@{ast.name}") + self.asm.instr("D=A") + def immediate(self, ast: Expr) -> Optional[int]: """If the expression is a constant which the ALU can take as an "immediate" operand (i.e. -1, 0, or 1), then unpack it. @@ -1520,6 +2063,18 @@ def immediate(self, ast: Expr) -> Optional[int]: else: return None + def symbol(self, ast: Expr) -> Optional[str]: + """If the expression is a reference to a Reg or Static which has a known (symbolic) address, + then construct it. + """ + if isinstance(ast, Reg): + return f"R{5+ast.idx}" + elif isinstance(ast, Static): + return f"{self.class_namespace}.static_{ast.name}" + else: + return None + + def describe_expr(self, expr) -> str: """A short suffix categorizing the type of expression, for example 'const'. @@ -1535,25 +2090,41 @@ def describe_expr(self, expr) -> str: return "load" elif isinstance(expr, Reg): return "copy" + elif isinstance(expr, Static): + return "copy" # Confusing? These "loads" are as efficient as reg-reg copies + elif isinstance(expr, Temp): + return "direct" elif isinstance(expr, Binary): return "binary" elif isinstance(expr, Unary): return "unary" elif isinstance(expr, IndirectRead): return "read" + elif isinstance(expr, Symbol): + return "symbol" + elif isinstance(expr, Invoke): + return "invoke" else: raise Exception(f"Unknown expr: {expr}") # Helpers: - def value_to_a(self, ast: Union[Reg, Const]): - """Load a register or constant value into A, without overwriting D, and in one less cycle, - in some cases.""" + def value_to_a(self, ast: Union[Reg, Static, Const]): + """Load a register, static or constant value into A, without overwriting D, and in one less cycle, + in some cases. + + Note: these values are essentially the types in Value, except Locals have been eliminated + at this point. + """ if isinstance(ast, Reg): self.asm.instr(f"@R{5+ast.idx}") self.asm.instr("A=M") + elif isinstance(ast, Static): + symbol_name = f"{self.class_namespace}.static_{ast.name}" + self.asm.instr(f"@{symbol_name}") + self.asm.instr("A=M") elif isinstance(ast, Const): if -1 <= ast.value <= 1: self.asm.instr(f"A={ast.value}") @@ -1563,7 +2134,7 @@ def value_to_a(self, ast: Union[Reg, Const]): self.asm.instr(f"@{-ast.value}") self.asm.instr("A=-A") else: - raise Exception(f"Unknown Value: {ast}") + raise Exception(f"Unexpected Value: {ast}") def _handle(self, ast): self.__getattribute__(f"handle_{ast.__class__.__name__}")(ast) @@ -1598,7 +2169,6 @@ def _compare(self, op): def _return(self): "Override the normal sequence; much less stack adjustment required." - # TODO: use this only for non-leaf subroutines label = self.asm.next_label("return_common") @@ -1641,6 +2211,8 @@ def _return(self): self.asm.instr("A=M") self.asm.instr("0;JMP") + self.asm.blank() + return label @@ -1675,7 +2247,10 @@ def _Stmt_str(stmt: Stmt) -> str: elif isinstance(stmt, While): return f"while ({'; '.join(_Stmt_str(s) for s in stmt.test)}; {_Expr_str(stmt.value)} {stmt.cmp} zero)\n" + jack_ast._indent("\n".join(_Stmt_str(s) for s in stmt.body)) elif isinstance(stmt, Return): - return f"return {_Expr_str(stmt.expr)}" + if stmt.expr is not None: + return f"return {_Expr_str(stmt.expr)}" + else: + return "return" elif isinstance(stmt, Push): return f"push {_Expr_str(stmt.expr)}" elif isinstance(stmt, Discard): @@ -1701,12 +2276,20 @@ def _Expr_str(expr: Expr) -> str: return f"{expr.name} ({expr.kind} {expr.idx})" elif isinstance(expr, Reg): return f"{expr.name} (r{expr.idx})" + elif isinstance(expr, Static): + return f"{expr.name} (static)" + elif isinstance(expr, Temp): + return f"{expr.name} (D)" elif isinstance(expr, Binary): return f"{_Expr_str(expr.left)} {expr.op.symbol} {_Expr_str(expr.right)}" elif isinstance(expr, Unary): return f"{expr.op.symbol} {_Expr_str(expr.value)}" elif isinstance(expr, IndirectRead): return f"mem[{_Expr_str(expr.address)}]" + elif isinstance(expr, Symbol): + return f"&{expr.name}" + elif isinstance(expr, Invoke): + return f"call (* {_Expr_str(expr.ptr)})" else: raise Exception(f"Unknown Expr: {expr}") diff --git a/alt/scheme/Interpreter.jack b/alt/scheme/Interpreter.jack new file mode 100644 index 0000000..4f42380 --- /dev/null +++ b/alt/scheme/Interpreter.jack @@ -0,0 +1,1050 @@ +class Interpreter { + // For any kind of efficiency, the compiler wants to allocate these pointers as statics in the + // usual range (starting at addr 16). And ordinarily its own stack state is stored in + // locations 0-3 or so. + // For that matter, its stack grows up from 256 (which is fine as long as it doesn't collide + // with the screen buffer at 2048.) + static Rib stack; + static Rib pc; + static Rib nextRib; + + // Pointers to the functions that handle each primitive: + static Array handlers; + + // + // IO + // + + static int cursorX; + static int cursorY; + + static Array bufferStart; + static Array bufferEnd; + + // Code of the key that was returned by getchar on the most recent call. + static int lastKeyPressed; + + static Array tty; // DEBUG + + + /* + When we arrive here, the assembly prelude has already initialized many of the statics with the + starting values. + */ + function void main() { + // Register-allocated: + var int opcode; // only used to dispatch before any handling + var Rib symbol; // only used to find the value of a symbol + var Rib code; // temp used to extract proc's entry point + var Rib newStack; // temp used when assembling callee's stack + var Rib cont; // existing continuation + var Rib proc; // used only for dispatch + + // Stack-allocated: + var Rib savedProc; // saved to construct cont + var Rib newCont; // saved to update after constructing stack + var Rib symbolToUpdate; // need the location to update after .pop() + + // Initialize state: + + let tty = 4095; // DEBUG + + // Bottom of the "heap" area: + let nextRib = 32767 + 1; // Note: will overflow to -32768 + + do Interpreter.initSymbolTable(); + + do Interpreter.initPrimitiveVectors(); + + do Interpreter.initIO(); + + // Bottom of the stack: "primordial continuation" in ROM + let stack = Jack.symbol("rib_outer_cont"); + + // Skip the bogus "main" instr: + let pc = Jack.symbol("main"); + let pc = pc[2]; + + while (1) { + let opcode = pc[0]; + + if (opcode = 0) { + if (pc[2] = 0) { + // jump + + let symbol = Interpreter.getTarget(); // actually an entry or symbol + let proc = symbol[0]; + + if (proc[0] & (~31)) { // not between 0 and 31 + // closure + + let savedProc = proc; + + let cont = Interpreter.findContinuation(); + + // Note: can't overwrite the old continuation because it may be in ROM + let newCont = Interpreter.alloc(cont[0], savedProc, cont[2]); + + let stack = Interpreter.wrangleClosureParams(savedProc, newCont); + + let code = savedProc[0]; + let pc = code[2]; + } + else { + // primitive + + do Jack.invoke(handlers[proc[0]]); + + let cont = Interpreter.findContinuation(); + + // Overwrite top stack entry so the stack consists of the just-pushed + // result on top of the saved stack from the continuation. + let stack[1] = cont[0]; + + // PC = next instruction from the continuation + let pc = cont[2]; + } + } + else { + // call + + let symbol = Interpreter.getTarget(); // actually an entry or symbol + let proc = symbol[0]; + + if (proc[0] & (~31)) { // not between 0 and 31 + // closure + + let savedProc = proc; + + // New continuation: + // x = saved stack (after popping args) + // y = proc rib + // z = next instruction + let newCont = Interpreter.alloc(-1, proc, pc[2]); // tricky: faster to use proc from register in first call + let newStack = Interpreter.wrangleClosureParams(savedProc, newCont); + let newCont[0] = stack; + let stack = newStack; + + // Now jump to the entry point of the proc: + let code = savedProc[0]; + let pc = code[2]; + } + else { + // primitive + + do Jack.invoke(handlers[proc[0]]); + + let pc = pc[2]; + } + } + } + else { + if (opcode = 1) { + // set + + let symbolToUpdate = Interpreter.getTarget(); // actually an entry or symbol + + let symbolToUpdate[0] = Interpreter.pop(); + + let pc = pc[2]; + } + else { + if (opcode = 2) { + // get + + let symbol = Interpreter.getTarget(); // actually an entry or symbol + + do Interpreter.push(symbol[0]); + + let pc = pc[2]; + } + else { + if (opcode = 3) { + // const + + do Interpreter.push(pc[1]); + + let pc = pc[2]; + } + else { + if (opcode = 4) { + // if + + if (Interpreter.pop() = Jack.symbol("rib_false")) { + let pc = pc[2]; + } + else { + let pc = pc[1]; + } + } + else { + // if (opcode = 5) { + // halt + do Interpreter.halt(); + // } + }}}}} + } + + return; + } + + + + /** + Allocate symbols (which must be in RAM so their values can be updated in place) and the + pairs that form the list that is the symbol table. + The decoded instructions in ROM refer to the addresses where these symbols are expected to + located in memory. + */ + function void initSymbolTable() { + var Array ptr; + var Rib symbol; + var Rib entry; + + let entry = Jack.symbol("rib_nil"); + let ptr = Jack.symbol("symbol_names_start"); + while (ptr < Jack.symbol("symbol_names_end")) { + // let tty[0] = ptr; // DEBUG + let symbol = Interpreter.alloc(ptr[1], ptr[0], 2); // symbol type + let entry = Interpreter.alloc(symbol, entry, 0); // pair type + let ptr = ptr + 2; + } + + return; + } + + + function void initPrimitiveVectors() { + // Just below the screen buffer + let handlers = Jack.symbol("SCREEN") - 32; + + let handlers[ 0] = Jack.symbol("interpreter.handleRib"); + let handlers[ 1] = Jack.symbol("interpreter.handleId"); + let handlers[ 2] = Jack.symbol("interpreter.handleArg1"); + let handlers[ 3] = Jack.symbol("interpreter.handleArg2"); + let handlers[ 4] = Jack.symbol("interpreter.handleClose"); + let handlers[ 5] = Jack.symbol("interpreter.handleRibQ"); + let handlers[ 6] = Jack.symbol("interpreter.handleField0"); + let handlers[ 7] = Jack.symbol("interpreter.handleField1"); + let handlers[ 8] = Jack.symbol("interpreter.handleField2"); + let handlers[ 9] = Jack.symbol("interpreter.handleField0_set"); + let handlers[10] = Jack.symbol("interpreter.handleField1_set"); + let handlers[11] = Jack.symbol("interpreter.handleField2_set"); + let handlers[12] = Jack.symbol("interpreter.handleEqvQ"); + let handlers[13] = Jack.symbol("interpreter.handleLt"); + let handlers[14] = Jack.symbol("interpreter.handlePlus"); + let handlers[15] = Jack.symbol("interpreter.handleMinus"); + let handlers[16] = Jack.symbol("interpreter.handleTimes"); + let handlers[17] = Jack.symbol("interpreter.handleQuotient"); + let handlers[18] = Jack.symbol("interpreter.handleGetchar"); + let handlers[19] = Jack.symbol("interpreter.handlePutchar"); + let handlers[20] = Jack.symbol("interpreter.handlePeek"); + let handlers[21] = Jack.symbol("interpreter.handlePoke"); + let handlers[22] = Jack.symbol("interpreter.handleHalt"); + let handlers[23] = Jack.symbol("interpreter.handleScreenAddr"); + + // Extra, just to simplify the dispatching logic: + let handlers[24] = Jack.symbol("interpreter.handleUnimp"); + let handlers[25] = Jack.symbol("interpreter.handleUnimp"); + let handlers[26] = Jack.symbol("interpreter.handleUnimp"); + let handlers[27] = Jack.symbol("interpreter.handleUnimp"); + let handlers[28] = Jack.symbol("interpreter.handleUnimp"); + let handlers[29] = Jack.symbol("interpreter.handleUnimp"); + let handlers[30] = Jack.symbol("interpreter.handleUnimp"); + let handlers[31] = Jack.symbol("interpreter.handleUnimp"); + + return; + } + + function void initIO() { + // Allocate space for one full line of characters, just below where the primitive vectors are stored + let bufferStart = handlers - 80; // a constant + let bufferEnd = bufferStart; // ptr to the word *after* the last word containing a character + + let cursorX = 0; + let cursorY = 0; + let lastKeyPressed = 0; + + return; + } + + /* rib :: x y z -- rib(x, y, z) */ + function void handleRib() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage (top entry of stack) + + let z = stack[0]; + + let tmp = stack[1]; + let y = tmp[0]; + + // The entry holding x will be the new top of stack: + let stack = tmp[1]; + let x = stack[0]; + + // Now re-use the second entry's rib as the newly-constructed rib: + let tmp[0] = x; + let tmp[1] = y; + let tmp[2] = z; + let stack[0] = tmp; + + return; + } + + /* id :: x -- x */ + function void handleId() { + return; + } + + /* arg1 :: x y -- x) # i.e. "drop" */ + function void handleArg1() { + let stack = stack[1]; + + return; + } + + /* arg2 :: x y -- y */ + function void handleArg2() { + var int y; + + // y = pop() + let y = stack[0]; + let stack = stack[1]; + + // replace(y) + let stack[0] = y; + + return; + } + + /* close :: x -- rib(x[0], stack, 1) */ + function void handleClose() { + var int x, y, z; + var Rib tmp; + + // Note: modifyinging the top entry on the stack in place, + // but allocating a new rib for the closure. + let tmp = stack[0]; + let stack[0] = Interpreter.alloc(tmp[0], stack[1], 1); + + return; + } + + /* rib? :: x -- bool(x is a rib) */ + function void handleRibQ() { + var int x, y, z; + var Rib tmp; + + let tmp = stack[0]; + if (Rib.isRib(tmp)) { + let stack[0] = Jack.symbol("rib_true"); + } + else { + let stack[0] = Jack.symbol("rib_false"); + } + + return; + } + + /* field0 :: rib(x, _, _) -- x */ + function void handleField0() { + var int x, y, z; + var Rib tmp; + + // No allocation: the top entry on the stack is updated in place + let tmp = stack[0]; + let stack[0] = tmp[0]; + + return; + } + + /* field1 :: rib(_, y, _) -- y */ + function void handleField1() { + var int x, y, z; + var Rib tmp; + + // No allocation: the top entry on the stack is updated in place + let tmp = stack[0]; + let stack[0] = tmp[1]; + + return; + } + + /* field2 :: rib(_, _, z) -- z */ + function void handleField2() { + var int x, y, z; + var Rib tmp; + + // No allocation: the top entry on the stack is updated in place + let tmp = stack[0]; + let stack[0] = tmp[2]; + + return; + } + + /* field0-set! :: rib(_, y, z) x -- x (and update the rib in place: rib(x, y, z)) */ + function void handleField0_set() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage (top entry of stack) + + let x = Interpreter.pop(); + let tmp = stack[0]; + let tmp[0] = x; + // Update the second entry on the stack in place: + let stack[0] = x; + + return; + } + + /* field1-set! :: rib(x, _, z) y -- y (and update the rib in place: rib(x, y, z)) */ + function void handleField1_set() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage (top entry of stack) + + let y = Interpreter.pop(); + let tmp = stack[0]; + let tmp[1] = y; + // Update the second entry on the stack in place: + let stack[0] = y; + + return; + } + + /* field2-set! :: rib(x, y, _) z -- z (and update the rib in place: rib(x, y, z)) */ + function void handleField2_set() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage (top entry of stack) + + let z = Interpreter.pop(); + let tmp = stack[0]; + let tmp[2] = z; + // Update the second entry on the stack in place: + let stack[0] = z; + + return; + } + + /* eqv? :: x y -- bool(x is identical to y) */ + function void handleEqvQ() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage (top entry of stack) + + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + if (x = y) { + let stack[0] = Jack.symbol("rib_true"); + } + else { + let stack[0] = Jack.symbol("rib_false"); + } + + return; + } + + /* < :: x y -- bool(x < y) */ + function void handleLt() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + // Update second stack entry in place: + if (x < y) { + let stack[0] = Jack.symbol("rib_true"); + } + else { + let stack[0] = Jack.symbol("rib_false"); + } + + return; + } + + /* + :: x y -- x + y */ + function void handlePlus() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + // Update second stack entry in place: + let stack[0] = x + y; + + return; + } + + /* - :: x y -- x - y */ + function void handleMinus() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + // Update second stack entry in place: + let stack[0] = x - y; + + return; + } + + /* * :: x y -- x * y */ + function void handleTimes() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + + // Update second stack entry in place: + let stack[0] = Interpreter.multiply(x, y); + + return; + } + + + /* quotient :: x y -- x / y */ + function void handleQuotient() { + var int x, y, z; + var Rib tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let x = stack[0]; + + // Update second stack entry in place: + let stack[0] = Interpreter.divide(x, y); + + return; + } + + + /* getchar :: -- (blocks an entire line is entered, while echoing characters) + + Once a line is captured, multiple calls will return successive characters until all are + consumed. Then the next call will block again. + + Note: this will only catch keypresses that occur after the instruction is executed. + */ + function void handleGetchar() { + var Array keyboard; + var int c, underscore; + var Array cursorAddr; + + if (~(bufferEnd > bufferStart)) { + let keyboard = Jack.symbol("KEYBOARD"); + let underscore = 95; + + while ((bufferEnd = bufferStart) | ~(c = 10)) { + + let cursorAddr = Interpreter.screenAddr(cursorX, cursorY); + let cursorAddr[0] = 0; + + // First wait for the key to be different than the last call/iteration: + let c = keyboard[0]; + while (c = lastKeyPressed) { + let cursorAddr[0] = underscore - cursorAddr[0]; // blink + let c = keyboard[0]; + } + + // Now wait for any key to be pressed: + while (c = 0) { + let cursorAddr[0] = underscore - cursorAddr[0]; // blink + let c = keyboard[0]; + } + + let lastKeyPressed = c; + + if (c = 128) { + // newline, in the weird HACK keymap + + // Clear the blicking cursor: + let cursorAddr[0] = 0; + + //move the cursor to the next line: + let cursorX = 0; + let cursorY = cursorY + 1; + + // Map to regular newline + let c = 10; + + // Add the + let bufferEnd[0] = c; + let bufferEnd = bufferEnd + 1; + } + else { if (c = 129) { + // backspace + if (bufferEnd > bufferStart) { + // Clear the blinking cursor: + let cursorAddr[0] = 0; + + // Move left and delete the last character from the buffer: + let cursorX = cursorX - 1; + let bufferEnd = bufferEnd - 1; + } + } + else { if (cursorX < 80) { + let cursorAddr[0] = c; + let cursorX = cursorX + 1; + let bufferEnd[0] = c; + let bufferEnd = bufferEnd + 1; + }}} + // TODO: if we're at the bottom of the screen, scroll everything up + } + + // An entire line has been captured; yield the first character and return: + do Interpreter.push(bufferStart[0]); + let bufferStart = bufferStart + 1; + + return; + } + else { + // Some characters were captured previously, so return the next in the sequence: + let c = bufferStart[0]; + let bufferStart = bufferStart+1; + if (~(bufferStart < bufferEnd)) { + // No more characters after this; reset the buffer + let bufferStart = handlers - 80; + let bufferEnd = bufferStart; + } + do Interpreter.push(c); + return; + } + } + + /* putchar :: c -- c (and draw the character at the current position) */ + function void handlePutchar() { + var int c; + var Array cursorAddr; + + // let c = Interpreter.peek(); + let c = stack[0]; + + if (c = 10) { + let cursorX = 0; + let cursorY = cursorY + 1; + // TODO: if we're at the bottom of the screen, scroll everything up + } + else { + let cursorAddr = Interpreter.screenAddr(cursorX, cursorY); + let cursorAddr[0] = c; + + let cursorX = cursorX + 1; + } + + return; + } + + /* peek :: x -- RAM[x] + + Note: the address is limited to the range of (tagged) int. It certainly can address low RAM, + including the screen buffer and the location for the keyboard/TTY. + */ + function void handlePeek() { + var int x; + var Array tmp; + + let tmp = 0; + + let x = stack[0]; + let stack[0] = tmp[x]; + + return; + } + + /* poke :: x y -- y (and write the value y at RAM[x]) + + Note: the address is limited to the range of (tagged) int. It certainly can address low RAM, + including the screen buffer and the location for the keyboard/TTY. + */ + function void handlePoke() { + var int y; + var Array tmp; + + // Note: one rib becomes garbage: + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + + let tmp = stack[0]; + let tmp[0] = y; + + // Update the second stack entry in place + let stack[0] = y; + + return; + } + + /* halt :: -- (no more instructions are executed) */ + function void handleHalt() { + do Interpreter.halt(); + } + + /* screenAddr :: x y -- address of character at column x, line y + + Cheat: calculate the address of a character in the screen buffer, using + hand-coded "shift" and adds to multiply by 80, and without any allocation. + + Doing its own (interpreter) stack manipulation to keep variables local and avoid arguments + on the (Jack) stack, and doing it by hand so this can be a leaf function. + + TODO: implement function inlining and just use pop/peek/replace + */ + function void handleScreenAddr() { + var int x, y, addr; + // var int x, y, acc0, acc1, acc2, acc3, acc4, acc5, acc6, addr; + + // let tty[0] = -42; + + // let y = Interpreter.pop(); + let y = stack[0]; + let stack = stack[1]; + // let x = Interpreter.pop(); + let x = stack[0]; // i.e. peek() + + // // 80x = 8*(4x + x) = 2(2(2(2(2(2x) + x)))) + // let acc0 = y + y; // 2y + // let acc1 = acc0 + acc0; // 4y + // let acc2 = acc1 + y; // 5y + // let acc3 = acc2 + acc2; // 10y + // let acc4 = acc3 + acc3; // 20y + // let acc5 = acc4 + acc4; // 40y + // let acc6 = acc5 + acc5; // 80y + + // let addr = 2048 + acc6 + x; + + let addr = Interpreter.screenAddr(x, y); + + // do Interpreter.push(addr); + let stack[0] = addr; // replace(); + + return; + } + + function Array screenAddr(int x, int y) { + var int acc0, acc1, acc2, acc3, acc4, acc5, acc6; + + // 80x = 8*(4x + x) = 2(2(2(2(2(2x) + x)))) + let acc0 = y + y; // 2y + let acc1 = acc0 + acc0; // 4y + let acc2 = acc1 + y; // 5y + let acc3 = acc2 + acc2; // 10y + let acc4 = acc3 + acc3; // 20y + let acc5 = acc4 + acc4; // 40y + let acc6 = acc5 + acc5; // 80y + + return 2048 + acc6 + x; + } + + /* Handler for any unused primitive code. */ + function void handleUnimp() { + // TODO + do Interpreter.halt(); + } + + /** + Decode the "y" value from a jump/call, set, or get instruction, and return the rib that + contains the target, which might be a stack entry or a symbol. In either case, the actual + target is found in the "x" field of the result. + */ + function Rib getTarget() { + var int slotOrGlobal; + var int i; + var Rib ptr; + + let slotOrGlobal = pc[1]; + if ((slotOrGlobal > -1) & (slotOrGlobal < 1000)) { + // y is slot # of target + let i = slotOrGlobal; + let ptr = stack; + while (i > 0) { + let ptr = ptr[1]; + let i = i - 1; + } + return ptr; + } + else { + // y is addr of target symbol + return slotOrGlobal; + } + } + + /** + Pop numArgs objects from the stack, assembling them into a new stack (in reverse order), + on top of the just-allocated continuation rib. + + Note aggressive manual inlining here to reduce call overhead because this + is part of the most expensive code path. + */ + function Rib wrangleClosureParams(Rib proc, Rib cont) { + var Rib code; + var int numArgs; + var int x; + var Rib newStack; + + let code = proc[0]; + let numArgs = code[0]; + let newStack = cont; + while (numArgs > 0) { + // let x = Interpreter.pop(); + let x = stack[0]; + let stack = stack[1]; + + // Note: can't re-use the stack entries, because they seem to be shared + // with captured environment in some cases: + // let newStack = Interpreter.alloc(x, newStack, 0); + let nextRib[0] = x; + let nextRib[1] = newStack; + let nextRib[2] = 0; + let newStack = nextRib; + let nextRib = nextRib + 3; + + let numArgs = numArgs - 1; + } + return newStack; + } + + /** + This was lifted verbatim from Math.jack in the solutions for project 12 from the book, except + for inlining abs(). + Does that mean it's good? Maybe, but it probably works. + */ + function int multiply(int x, int y) { + var int localX, localY; + var boolean neg; + var int tmp; + var int sum, shiftedX, shiftedBit; + + // local copies for quick access + let localX = x; + let localY = y; + + // Get sign, then take absolute values (inline to save fn calls): + // Note: testing each operand only once to minimize branching. + let neg = false; + if (localY < 0) { + let localY = -localY; + let neg = ~neg; + } + if (localX < 0) { + let localX = -localX; + let neg = ~neg; + } + + // Put the smaller (abs.) value in y (because this is O(log y)): + if (localX < localY) { + let tmp = localY; + let localY = localX; + let localX = tmp; + } + + let sum = 0; + let shiftedX = localX; + let shiftedBit = 1; + + while ((shiftedBit > 0) & ~(shiftedBit > localY)) { + if (localY & shiftedBit) { + let sum = sum + shiftedX; + } + let shiftedX = shiftedX + shiftedX; + let shiftedBit = shiftedBit + shiftedBit; + } + + // Check the original signs and adjust the result: + if (neg) { + return -sum; + } + else { + return sum; + } + } + + /** + This was lifted verbatim from Math.jack in the solutions for project 12 from the book, except + for inlining abs(). + Does that mean it's good? Maybe, but it probably works. + */ + function int divide(int x, int y) { + var boolean neg; + var int q, r; + + if (y = 0) { + do Interpreter.halt(); + } + + // Get sign, then take absolute values (inline to save fn calls): + // Note: testing each operand only once to minimize branching. + let neg = false; + if (y < 0) { + let y = -y; + let neg = ~neg; + } + if (x < 0) { + let x = -x; + let neg = ~neg; + } + + // No more bits to look at: + if (y > x) { + return 0; + } + + // Try dividing by 2y: + if ((y+y) < 0) { + // If 2*y overflows, it's definitely > x. + let q = 0; + } + else { + let q = Interpreter.dividePos(x, y+y); + } + let r = q+q; + if (~((x - Interpreter.multiply(r, y)) < y)) { + let r = r + 1; + } + + if (neg) { + return -r; + } + else { + return r; + } + } + + /** Divide x by y, assuming both a positive. */ + function int dividePos(int x, int y) { + var int q, r; + + // No more bits to look at: + if (y > x) { + return 0; + } + + // Try dividing by 2y: + if ((y+y) < 0) { + // If 2*y overflows, it's definitely > x. + let q = 0; + } + else { + let q = Interpreter.dividePos(x, y+y); + } + let r = q+q; + if (~((x - Interpreter.multiply(r, y)) < y)) { + let r = r + 1; + } + + return r; + } + + /** Allocate a rib on the heap, filling in the three fields. */ + function Rib alloc(int x, int y, int z) { + var Rib r; + + // Note: makes this not a leaf function, but most callers have already inlined it anyway + do Interpreter.checkHeap(); + + let r = nextRib; + let r[0] = x; + let r[1] = y; + let r[2] = z; + let nextRib = nextRib + 3; + return r; + } + + /** + Make sure there's space to allocate a rib, otherwise halt. + + TODO: make space by collecting garbage. + */ + function void checkHeap() { + if (~Rib.isRib(nextRib)) { + do Interpreter.halt(); + } + else { + return; + } + } + + function void push(int obj) { + // let stack = Interpreter.alloc(obj, stack, 0); // pair-type + + // Avoid function call overhead by doing the allocation directly here: + let nextRib[0] = obj; + let nextRib[1] = stack; + let nextRib[2] = 0; // pair type + let stack = nextRib; + let nextRib = nextRib+3; + + return; + } + + /** Discard the top entry from the stack, return its CAR. */ + // TODO: get the compiler to inline this + function int pop() { + var int r; + let r = stack[0]; + let stack = stack[1]; + return r; + } + + // /** Get the object on the top of the stack, without removing it. */ + // // TODO: get the compiler to inline this + // function int peek() { + // return stack[0]; + // } + + // /** Overwrite the object on the top of the stack, avoiding allocation. */ + // // TODO: get the compiler to inline this + // function void replace(int obj) { + // let stack[0] = obj; + // return; + // } + + /** + Address of the continuation rib in the current stack frame: the first entry in the + stack with z != 0. + */ + function Rib findContinuation() { + var Rib ptr; + + let ptr = stack; + while (ptr[2] = 0) { + let ptr = ptr[1]; + } + return ptr; + } + + function void halt() { + while (1) { + // let tty[0] = -1; // DEBUG + } + } +} diff --git a/alt/scheme/README.md b/alt/scheme/README.md new file mode 100644 index 0000000..63e78f6 --- /dev/null +++ b/alt/scheme/README.md @@ -0,0 +1,137 @@ +# Scheme + +Scheme interpreter, based on [Ribbit](https://github.com/udem-dlteam/ribbit/). + +Goals: +- run on the vanilla Hack architecture (well, the CPU anyway) +- provide a REPL that can compile and run simple functions like `fib` or `fact` written at the + keyboard +- run something graphical (even if in character-mode) +- implement as little as possible in assembly/Jack; just the bare interpreter and Ribbit primitives, + plus a few additions for accessing the hardware. + +![evaluating (+ 1 2)](capture.mov) + + +## Virtual Machine + +A source program is compiled by the Ribbit AOT compiler to an instruction graph as specified in +[A Small Scheme VM, Compiler, and REPL in 4K](http://www.iro.umontreal.ca/~feeley/papers/YvonFeeleyVMIL21.pdf), +with a few modifications. + +Additional primitives: +- `peek`; code: `20`; `x ← pop(); r ← RAM[x]` +- `poke`; code: `21`; `y ← pop(); x ← pop(); RAM[x] ← y; r <- y` +- `halt`; code: `22`; stop the machine, by going into a tight loop recognized by the simulator +- `screenAddr`; code: `23`; `y <- pop(); x <- pop(); r <- address of character at (x, y)` + +`peek` and `poke` can be used to read and write to any address, but most usefully the screen buffer +(which is at 0x0400–0x0x07E7) and the location mapped to the keyboard (0x07FF). + +`halt` can be used to signal normal completion of a program. + +`screenAddr` provides a fast implementation of the very common calculation mapping coordinates +to the screen buffer in memory. + +Note: `getchar` reads characters one at a time from a buffer which contains up to an entire line. +No characters are returned until `newline` (HACK: 128; Scheme: 10) is entered. As long as a line +hasn't been completed, the backspace key can be used to edit the line by removing the last-entered +character (if any). +However, at present, characters can only be entered after `getchar` has been invoked. While in the +loop receiving keypresses, a "blinking" underscore indicates the cursor position. +This makes `getchar`/`putchar` unsuitable for non-terminal-oriented uses (i.e. games.) Instead, you +can implement your own "keyDown" and "drawChar" operations using `(peek 4095)` and +`(poke (screenAddr x y) c)` (see [io.scm](io.scm)). + + +## Memory Layout + +| Address | Label | Contents | Description | +| 0 | `interpreter.static_stack` | SP | Address of the cons list that represents the stack: i.e. a "pair" rib which contains the value at the top of the stack and points to the next entry. | +| 1 | `interpreter.static_pc` | PC | Address of the rib currently being interpreted. | +| 2 | `interpreter.static_nextRib` | NEXT_RIB | Address where the next allocation will occur. | +| ... | | | TBD: values used by the garbage collector to keep track of free space. | +| 256– | | | Jack stack. | +| 1936–2015 | `interpreter.static_bufferStart`/`End` | | Space to store a line of input from the keyboard. | +| 2016–2047 | `interpreter.static_handlers` | | Function pointer for each primitive handler. | +| 2048-4048 | | | Screen buffer: 80x25 characters. | +| 4095 | | KEYBOARD | Mapped to the keyboard for input and "tty" for output. | +| 4096–32767 | | | ROM: interpreter, symbol names, instructions. | +| 32768–65536 | | | Heap. | + +*Note: when the Jack interpreter is used, these locations are determined by the assembler, +addresses 0-15 and are used by the interpreter's own stack pointers and temporary storage, and the +interpreter's own stack grows up from address 256.* + + +## Rib Representation + +For simplicity, ribs are stored as described in the paper, three words each. + +**TODO**: The high bit of each word is used as a tag to identify whether the word points to a rib. +- A word with the high bit set is a 15-bit signed integer value. To recover an ordinary 16-bit value when + needed, bit 14 is copied to bit 15. For many operations, it's sufficient to treat values as unsigned + and just make sure to set the high bit if it might have been cleared (e.g. due to overflow.) +- A word with the high bit unset is the address of a rib in memory, treated as unsigned and + divided by three, so that every possible rib address fits in the range of 15-bit unsigned values. + +Note: Ribbit's C implementation uses the *low* bit to tag integers, but that's not practical on this +CPU, which does not provide a right-shift instruction. Masking off the high bit can be done efficiently. +On the other hand, rib addresses only ever need to be generated incrementally so we only need the +`extract` operation: multiply by 3 (with two "+" instructions.) + +Future: +- really squeeze the bits and get each rib into 2 words. If possible, this saves 33% of the space, + and adds a *lot* of cycles to decode the bits in the interpreter. And it might mean reducing + the maximum number of ribs that can be addressed, which defeats the purpose. + + +## Initialization + +To make efficient use of available memory, the symbol table and instruction graph produced by +Ribbit's compiler (`rsc.py`) are decoded into ribs and included as "data" words in the ROM, using +[big.py](../big.py)'s extra `#` opcode. + +The actual `symbol` ribs that make up the symbol table have to be mutable, so they're constructed +in RAM during initialization. Their addresses are known ahead of time, so the code in ROM refers to +them directly. + +Note: the REPL consumes about 2K ribs in memory, and takes about 2KB of encoded data in the Ribbit +implementations. That means to initialize we need something like 6K of heap plus 2K words, plus +some for stack. + +Actual size in the implementation: +- Instruction ribs in the ROM for the REPL: 1,340 (4.0K words) +- String/char ribs in R0M for the symbol table: 1,170 (3.5K words) +- Initial ribs in RAM for the symbol table: 89 (267 words) +- Actual total ROM, including interpreter and tables: at least 11K. + + +[A comment in the Ribbit source](https://github.com/udem-dlteam/ribbit/blob/dev/src/host/c/rvm.c#L207) +suggests that space for 48,000 ribs is needed to bootstrap. Presumably that refers to running the +*compiler*, which is out of scope. + + +## GC + +The program runs until memory is exhausted. + +**TODO**: collect garbage. + +See https://github.com/udem-dlteam/ribbit/issues/26. + + +## Performance + +With the current Jack compiler and the `compiled` simulator, performance of the interpreter is good +enough to call interactive. However, if and when we decide to improve it, there are a couple of +obvious bottlenecks: + +- Symbol table lookup: the REPL's symbol table is at least 89 deep, and linear search in interpreted + Scheme is currently consuming a large portion of the total time. Could experiment with a simple tree + structure, which would be roughly log(90) = 6 levels deep. + +- Dispatching on instruction and primitive codes is relatively expensive. Could embed handler pointers + directly in the ribs? Or just use more codes to flatten the amount of dispatching that's currently + needed (e.g. separate codes for call slot, call symbol, jump to slot, jump to symbol). Either would + require modifying the Scheme compiler to account for the new representation. diff --git a/alt/scheme/Rib.jack b/alt/scheme/Rib.jack new file mode 100644 index 0000000..7677f3f --- /dev/null +++ b/alt/scheme/Rib.jack @@ -0,0 +1,22 @@ +class Rib { + field int x, y, z; + + /** + TEMP: for now every rib pointer is definitely above ROM_BASE (4096), so any value smaller + than that is considered an un-boxed int. We'll also treat negative values in the same range + as raw ints. + This is all bogus. Need to implement a proper tagging scheme so we can use larger int values + (i.e. 15 bits.) + */ + // TODO: get the compiler to inline this + function boolean isRib(int obj) { + return (obj < -4095) | (obj > 4095); + } + + // TODO: get the compiler to inline this, so we can actually afford to use it + // method int x() { + // return x; + // } + + // TODO: y(), z(), setX/Y/Z()? +} diff --git a/alt/scheme/capture.mov b/alt/scheme/capture.mov new file mode 100644 index 0000000..885b9c2 Binary files /dev/null and b/alt/scheme/capture.mov differ diff --git a/alt/scheme/example/echo.scm b/alt/scheme/example/echo.scm new file mode 100644 index 0000000..cb7ca89 --- /dev/null +++ b/alt/scheme/example/echo.scm @@ -0,0 +1,10 @@ +;; Read and echo input line by line. +;; Note: getchar buffers the input until a whole line is entered; this script just has to read +;; and write the characters one at a time. + +(define (echo) + (let ((c (getchar))) + (putchar c) + (echo))) + +(echo) diff --git a/alt/scheme/example/game.scm b/alt/scheme/example/game.scm new file mode 100644 index 0000000..2888bfa --- /dev/null +++ b/alt/scheme/example/game.scm @@ -0,0 +1,35 @@ +;; Move a character around the screen, controlled by the arrow keys. +;; +;; Ok, so it's not much of a game. And there's a fair amount of flicker. +;; And it consumes memory just waiting for a key to be pressed, because +;; it doesn't use the optimized, but terminal-oriented getchar primitive. +;; +;; Requires min.scm and io.scm + +(define x 39) +(define y 12) + +(define (game) + (let ((c (peek 4095))) + (cond + ((= c 130) + (drawchar x y 0) + (set! x (- x 1)) + (drawchar x y 42)) + ((= c 131) + (drawchar x y 0) + (set! y (- y 1)) + (drawchar x y 42)) + ((= c 132) + (drawchar x y 0) + (set! x (+ x 1)) + (drawchar x y 42)) + ((= c 133) + (drawchar x y 0) + (set! y (+ y 1)) + (drawchar x y 42)) + ) + (game))) + +(drawchar x y 42) +(game) diff --git a/alt/scheme/example/output.scm b/alt/scheme/example/output.scm new file mode 100644 index 0000000..11efac0 --- /dev/null +++ b/alt/scheme/example/output.scm @@ -0,0 +1,77 @@ +(define poke (rib 21 0 1)) + +;; (define screen 2048) +;; (define (drawchar x y c) (poke (+ screen (+ x (* 80 y))) c)) + +(define screenAddr (rib 23 0 1)) +(define (drawchar x y c) (poke (screenAddr x y) c)) + +;; First, ABCD in the corners of the screen: +(drawchar 0 0 65) +(drawchar 79 0 66) +(drawchar 0 24 67) +(drawchar 79 24 68) + +;; About 40k cycles and 2% of the heap to get this far, without a primitive * +;; Down to 16K cycles and 0.8% of the heap using primitive * + + + +;; Now, a character map table: +;; 00 01 02 ... +;; ... +;; 30 0 1 2 ... +;; 40 @ A B ... +;; ... + +(define char-zero 48) +(define char-a 65) +(define (hex c) (if (< c 10) (+ c char-zero) (+ c (- char-a 10)))) + +;; Effectful loop: evaluate (f value) for each x <= value < y, discarding the result. +(define (for x y f) + (if (< x y) + (begin + (f x) + (for (+ x 1) y f)) + '())) + +;; Header row: 00 01 02 ... +(for 0 16 (lambda (x) + (begin + (drawchar (+ 5 (* 3 x)) 3 char-zero) + (drawchar (+ 6 (* 3 x)) 3 (hex x))))) + +;; About 200k cycles and 10% of the heap to this point + +;; Header column: 00, 10, ... 70 +(for 0 8 (lambda (y) + (begin + (drawchar 1 (+ y 4) (hex y)) + (drawchar 2 (+ y 4) char-zero)))) + +;; Fill in only the rows with printable chars: 2-7 +(for 2 8 (lambda (y) + (let ((h (* 16 y))) + (for 0 16 (lambda (x) + (drawchar + (+ 6 (* 3 x)) + (+ y 4) + (+ h x))))))) + +;; 1.1M cycles and 40% of the heap to complete (assembly interpreter) + +;; 5.3M cycles and 38.4% of the heap (Jack interpreter) +;; 3.6M cycles after optimizing leaf functions in reg.py +;; 3.5M cycles after collapsing stores with simple expressions +;; 3.3M cycles after unifying codegen for registers and statics +;; [2.9M cycles after totally overhauling flattening (and probably introducing lots of bugs)] +;; 3.1M cycles after reducing use of arguments/locals in getTarget and handlePrimitive +;; 3.0M cycles after splitting locals in main() so most are in registers +;; 2.9M cycles after splitting savedProc from proc for dispatch +;; 2.8M cycles after assigning test values for if/while to D +;; 2.5M cycles after assigning memory read addresses to D +;; 2.4M cycles after assigning sources for binary/comp exprs to D +;; 2.1M cycles and 36% of the heap after adding a "screenAddress" primitive +;; 1.8M cycles after inlining calls in "call" sequence (wrangleClosureParams) +;; 1.6M cycles after replacing the big nested dispatch with a table of function pointers diff --git a/alt/scheme/inspector.py b/alt/scheme/inspector.py new file mode 100644 index 0000000..a597834 --- /dev/null +++ b/alt/scheme/inspector.py @@ -0,0 +1,209 @@ +"""Python wrappers for Scheme types, for tracing purposes.""" + +from nand.vector import extend_sign, unsigned +from alt import big + +# Seems like overkill +SHOW_SAVED_STACKS = False + +class Inspector: + def __init__(self, computer, symbols, stack_loc, rom_base=big.ROM_BASE, rom_limit=big.HEAP_BASE): + self.stack_loc = stack_loc + + def peek(addr): + if rom_base <= addr < rom_limit: + return computer.peek_rom(addr) + else: + return computer.peek(addr) + + self.peek = peek + self.symbols_by_addr = { addr: name for (name, addr) in symbols.items() } + + + def show_addr(self, addr): + """Show an address (with "@"), using the symbol for addresses in ROM.""" + if addr in self.symbols_by_addr: + return f"@{self.symbols_by_addr[addr]}" + else: + return f"@{unsigned(addr)}" + + def is_labeled(self, addr): + return addr in self.symbols_by_addr + + def show_instr(self, addr): + x, y, z = self.peek(addr), self.peek(addr+1), self.peek(addr+2) + + def show_target(with_value): + """The target of a jump/call, get, or set: either a slot index or global.""" + + # FIXME: use the rvm's actual/current value + MAX_SLOT = big.ROM_BASE-1 + if 0 <= y <= MAX_SLOT: + return f"#{y}" + else: + return self.show_symbol(y, with_value=with_value) + + if x == 0 and z == 0: + return f"jump {show_target(True)}" + elif x == 0: + return f"call {show_target(True)}" # -> {self.show_addr(z)}" + elif x == 1: + return f"set {show_target(False)}" # -> {self.show_addr(z)}" + elif x == 2: + return f"get {show_target(False)}" # -> {self.show_addr(z)}" + elif x == 3: + return f"const {self.show_obj(y)}" # -> {self.show_addr(z)}" + elif x == 4: + return f"if -> {self.show_addr(y)} else {self.show_addr(z)}" + elif x == 5: + return "halt" + else: + return f"not an instr: {(x, y, z)}" + + + def show_symbol(self, addr, with_value=False): + val, name, z = self.peek(addr), self.peek(addr+1), self.peek(addr+2) + assert z == 2 + rib_num = (addr - big.HEAP_BASE)//3 + name_and_addr = f"{self._obj(name)}{self.show_addr(addr)}(_{rib_num})" + if with_value: + return f"{name_and_addr} = {self._obj(val)}" + else: + return name_and_addr + + + def _obj(self, val, max_depth=10): + """Python representation of an object, which may be an integer, special value, or rib.""" + + # FIXME: check tag + if -big.ROM_BASE < extend_sign(val) < big.ROM_BASE: + return extend_sign(val) + elif self.symbols_by_addr.get(val) == "rib_nil": + return [] + elif self.symbols_by_addr.get(val) == "rib_true": + return True + elif self.symbols_by_addr.get(val) == "rib_false": + return False + else: + x, y, z = self.peek(val), self.peek(val+1), self.peek(val+2) + if z == 0: # pair + car = self._obj(x) + if max_depth > 0: + cdr = self._obj(y, max_depth=max_depth-1) + else: + cdr = ["..."] + if isinstance(cdr, list): + return [car] + cdr + else: + # In at least one case, this is a "jump" instruction during compilation + return (car, cdr, z) + elif z == 1: # proc + if 0 <= x < len(PRIMITIVES): + return f"proc({PRIMITIVES[x]})" + else: + num_args, instr = self.peek(x), self.peek(x+2) + return f"proc(args={num_args}, env={list_str(self.stack(y))}, instr={self.show_addr(instr)}){self.show_addr(val)}" + elif z == 2: # symbol + return f"symbol({self._obj(y)} = {self._obj(x)})" + elif z == 3: # string + chars = self._obj(x, max_depth=y) + if len(chars) != y: + print(f"bad string: {chars, y, z}") + return "".join(chr(c) for c in chars) + elif z == 4: # vector + elems = self._obj(x) + if not isinstance(elems, list) or len(elems) != y: + raw = (elems, self._obj(y), z) + print(f"bad vector: {raw}") + return raw + else: + return elems + elif z == 5: + # Unexpected, but show the contents just in case + return f"special({self.show_obj(x)}, {self.show_obj(y)}){self.show_addr(val)}" + else: + return f"TODO: ({x}, {y}, {z})" + + + def show_obj(self, val): + """Show an object, which may be an integer, special value, or rib.""" + + return str(self._obj(val)) + + + def stack(self, addr=None): + """Contents of the stack, bottom to top.""" + if addr is None: + addr = self.peek(self.stack_loc) + + if self.symbols_by_addr.get(addr) == "rib_nil": + # This appears only after the outer continuation is invoked: + return [] + elif addr == 0: + # sanity check + return ["<0>!"] + else: + x, y, z = self.peek(addr), self.peek(addr+1), self.peek(addr+2) + + if z == 0: # pair + return self.stack(y) + [self._obj(x)] + else: + if SHOW_SAVED_STACKS: + saved_str = list_str(self.stack(x)) + cont = f"cont(saved={saved_str}; {self.show_addr(z)}){self.show_addr(addr)}" + else: + cont = f"cont({self.show_addr(z)}){self.show_addr(addr)}" + + if y == 0: + # outer continuation: there is no proc, and no further stack + return [cont] + else: + # Note: no instruction ever actually looks at this entry. It's just holding `env`, + # which we're about to traverse. But need to show something so slot numbers make + # sense. + closure = "" + return self.stack(self.peek(y+1)) + [closure, cont] + + + def show_stack(self): + """Show the contents of the stack, which is a list composed of ordinary pairs and continuation + ribs.""" + + return ", ".join(str(o) for o in reversed(self.stack())) + + +def list_str(lst): + """Looks like a list's repr(), but use str() on the elements to avoid quoting everything. + + Why do I have to write this? Or do I? + """ + return "[" + ", ".join(str(x) for x in lst) + "]" + + +PRIMITIVES = { + 0: "rib", + 1: "id", + 2: "arg1", + 3: "arg2", + 4: "close", + 5: "rib?", + 6: "field0", + 7: "field1", + 8: "field2", + 9: "field0-set!", + 10: "field1-set!", + 11: "field2-set!", + 12: "eqv?", + 13: "<", + 14: "+", + 15: "-", + 16: "*", + 17: "quotient", + 18: "getchar", + 19: "putchar", + # Extra: + 20: "peek", + 21: "poke", + 22: "halt", + 23: "screenAddr", +} \ No newline at end of file diff --git a/alt/scheme/io.scm b/alt/scheme/io.scm new file mode 100644 index 0000000..95fca43 --- /dev/null +++ b/alt/scheme/io.scm @@ -0,0 +1,45 @@ +;; I/O "primitives" for the REPL and any other terminal-style programs. +;; +;; In this implementation, getchar and putchar aren't provided directly by the VM, because there's +;; no OS-level support for getting characters from the keyboard and onto the screen. Instead, the +;; VM provides a lower-level "getchar" (which is a blocking read of the location in memory where +;; the keyboard is mapped), generic "peek" and "poke" primitives which can read and write the +;; screen buffer, and (for peformance) a "screenAddr" primitive which calculates addresses in the +;; screen buffer memory faster than interpreted schems could and without allocation. + + +(define peek (rib 20 0 1)) +(define poke (rib 21 0 1)) + +;; (define screen 2048) +;; (define (drawchar x y c) (poke (+ screen (+ x (* 80 y))) c)) + +(define screenAddr (rib 23 0 1)) +(define (drawchar x y c) (poke (screenAddr x y) c)) + +;; (define cursorx 0) +;; (define cursory 0) +;; (define (putchar c) +;; (if (eqv? c 10) +;; (begin +;; (set! cursorx 0) +;; (set! cursory (+ 1 cursory))) +;; (begin +;; (drawchar cursorx cursory c) +;; (set! cursorx (+ 1 cursorx))))) + +;; The getchar primitive just blocks and then returns a non-zero char. The repl seems to +;; expect getchar to handle echo, etc. +;; TODO: use a (let ...) here to hide this definition +;; (define getchar-primitive (rib 18 0 1)) +;; (define (getchar) +;; (let ((c (getchar-primitive))) +;; (if (eqv? c 128) ;; newline, according to the strange key mapping +;; (begin +;; (set! cursorx 0) +;; (set! cursory (+ 1 cursory)) +;; 10) ;; regular ASCII newline +;; (begin +;; (putchar c) +;; c)))) + diff --git a/alt/scheme/repl.py b/alt/scheme/repl.py new file mode 100755 index 0000000..c714742 --- /dev/null +++ b/alt/scheme/repl.py @@ -0,0 +1,48 @@ +#! /usr/bin/env python + +"""Scheme REPL, reading input from the keyboard and writing output to the screen. + +Supported keywords and functions: +TBD +""" + +from alt.scheme import rvm + +def main(): + def load(path): + with open(f"alt/scheme/{path}") as f: + return f.readlines() + + + min_library_src_lines = load("ribbit/min.scm") + io_src_lines = load("io.scm") + + program = "".join(min_library_src_lines + io_src_lines) + """ + +(repl) + +;; Exported symbols. + +(export + + + - + * + quotient + < + = + cons + ) +""" + + # Note: actually running the compiler in the Ribbit Python interpreter is pretty slow. + # Probably want to cache the encoded result somewhere (or just go back to hard-coding it here.) + print("Compiling...") + + rvm.run(program, interpreter="jack", simulator="compiled", + print_asm=True, + trace_level=rvm.TRACE_NONE) + # trace_level=rvm.TRACE_COARSE) + + +if __name__ == "__main__": + main() diff --git a/alt/scheme/ribbit/min.scm b/alt/scheme/ribbit/min.scm new file mode 100644 index 0000000..d676262 --- /dev/null +++ b/alt/scheme/ribbit/min.scm @@ -0,0 +1,964 @@ +;; Ribbit Scheme runtime library. + +;; This is the "min" version with only the core R4RS predefined procedures. + +;;;---------------------------------------------------------------------------- + +;; Implementation of Ribbit Scheme types using the RVM operations. + +(define pair-type 0) +(define procedure-type 1) +(define symbol-type 2) +(define string-type 3) +(define vector-type 4) +(define singleton-type 5) + +(define (instance? type) (lambda (o) (and (rib? o) (eqv? (field2 o) type)))) + +;;;---------------------------------------------------------------------------- + +;; Booleans (R4RS section 6.1). + +(define (not x) (eqv? x #f)) + +;;(define (boolean? obj) (or (eqv? obj #t) (not obj))) + +;;;---------------------------------------------------------------------------- + +;; Equivalence predicates (R4RS section 6.2). + +;;(define eq? eqv?) + +(define (equal? x y) + (or (eqv? x y) + (and (rib? x) + (if (eqv? (field2 x) singleton-type) + #f + (and (rib? y) + (equal? (field2 x) (field2 y)) + (equal? (field1 x) (field1 y)) + (equal? (field0 x) (field0 y))))))) + +;;;---------------------------------------------------------------------------- + +;; Pairs and lists (R4RS section 6.3). + +(define pair? (instance? pair-type)) +(define (cons car cdr) (rib car cdr pair-type)) +(define car field0) +(define cdr field1) +(define set-car! field0-set!) +(define set-cdr! field1-set!) + +(define (cadr pair) (field0 (field1 pair))) +(define (cddr pair) (field1 (field1 pair))) +(define (caddr pair) (cadr (field1 pair))) +(define (cadddr pair) (caddr (field1 pair))) + +;;(define (caar pair) (field0 (field0 pair))) +;;(define (cadr pair) (field0 (field1 pair))) +;;(define (cdar pair) (field1 (field0 pair))) +;;(define (cddr pair) (field1 (field1 pair))) + +;;(define (caaar pair) (caar (field0 pair))) +;;(define (caadr pair) (caar (field1 pair))) +;;(define (cadar pair) (cadr (field0 pair))) +;;(define (caddr pair) (cadr (field1 pair))) +;;(define (cdaar pair) (cdar (field0 pair))) +;;(define (cdadr pair) (cdar (field1 pair))) +;;(define (cddar pair) (cddr (field0 pair))) +;;(define (cdddr pair) (cddr (field1 pair))) + +;;(define (caaaar pair) (caaar (field0 pair))) +;;(define (caaadr pair) (caaar (field1 pair))) +;;(define (caadar pair) (caadr (field0 pair))) +;;(define (caaddr pair) (caadr (field1 pair))) +;;(define (cadaar pair) (cadar (field0 pair))) +;;(define (cadadr pair) (cadar (field1 pair))) +;;(define (caddar pair) (caddr (field0 pair))) +;;(define (cadddr pair) (caddr (field1 pair))) +;;(define (cdaaar pair) (cdaar (field0 pair))) +;;(define (cdaadr pair) (cdaar (field1 pair))) +;;(define (cdadar pair) (cdadr (field0 pair))) +;;(define (cdaddr pair) (cdadr (field1 pair))) +;;(define (cddaar pair) (cddar (field0 pair))) +;;(define (cddadr pair) (cddar (field1 pair))) +;;(define (cdddar pair) (cdddr (field0 pair))) +;;(define (cddddr pair) (cdddr (field1 pair))) + +(define (null? obj) (eqv? obj '())) + +;;(define (list? obj) +;; (list?-aux obj obj)) + +;;(define (list?-aux fast slow) +;; (if (pair? fast) +;; (let ((fast (cdr fast))) +;; (cond ((eq? fast slow) +;; #f) +;; ((pair? fast) +;; (list?-aux (cdr fast) (cdr slow))) +;; (else +;; (null? fast)))) +;; (null? fast))) + +;;(define (list . args) args) + +(define (length lst) + (if (pair? lst) + (+ 1 (length (cdr lst))) + 0)) + +;;(define (append lst1 lst2) +;; (if (pair? lst1) +;; (cons (car lst1) (append (cdr lst1) lst2)) +;; lst2)) + +;;(define (reverse lst) +;; (reverse-aux lst '())) + +;;(define (reverse-aux lst result) +;; (if (pair? lst) +;; (reverse-aux (cdr lst) (cons (car lst) result)) +;; result)) + +(define (list-ref lst i) + (car (list-tail lst i))) + +(define (list-set! lst i x) + (set-car! (list-tail lst i) x)) + +(define (list-tail lst i) + (if (< 0 i) + (list-tail (cdr lst) (- i 1)) + lst)) + +;;(define (memv x lst) +;; (if (pair? lst) +;; (if (eqv? x (car lst)) +;; lst +;; (memv x (cdr lst))) +;; #f)) + +;;(define memq memv) + +;;(define (member x lst) +;; (if (pair? lst) +;; (if (equal? x (car lst)) +;; lst +;; (member x (cdr lst))) +;; #f)) + +;;(define (assv x lst) +;; (if (pair? lst) +;; (let ((couple (car lst))) +;; (if (eqv? x (car couple)) +;; couple +;; (assv x (cdr lst)))) +;; #f)) + +;;(define assq assv) + +;;(define (assoc x lst) +;; (if (pair? lst) +;; (let ((couple (car lst))) +;; (if (equal? x (car couple)) +;; couple +;; (assoc x (cdr lst)))) +;; #f)) + +(define (make-list k fill) + (make-list-aux k fill '())) + +(define (make-list-aux k fill lst) + (if (< 0 k) + (make-list-aux (- k 1) fill (cons fill lst)) + lst)) + +;;;---------------------------------------------------------------------------- + +;; Symbols (R4RS section 6.4). + +(define symbol? (instance? symbol-type)) +(define (string->uninterned-symbol str) (rib #f str symbol-type)) +(define symbol->string field1) +(define global-var-ref field0) +(define global-var-set! field0-set!) + +;; Symbol table. + +(define (string->symbol str) + (string->symbol-aux str symtbl)) + +(define (string->symbol-aux str syms) + (if (pair? syms) + (let ((sym (field0 syms))) + (if (equal? (field1 sym) str) + sym + (string->symbol-aux str (field1 syms)))) + (let ((sym (string->uninterned-symbol str))) + (set! symtbl (cons sym symtbl)) + sym))) + +(define symtbl (field1 rib)) ;; get symbol table + +(field1-set! rib 0) ;; release symbol table if not otherwise needed + +;;;---------------------------------------------------------------------------- + +;; Numbers (R4RS section 6.5). + +;;(define (integer? obj) (not (rib? obj))) + +;;(define rational? integer?) +;;(define real? rational?) +;;(define complex? real?) +;;(define number? complex?) + +;;(define (exact? obj) #t) +;;(define (inexact? obj) #f) + +(define = eqv?) +;;(define (> x y) (< y x)) +;;(define (<= x y) (not (< y x))) +;;(define (>= x y) (not (< x y))) + +;;(define (zero? x) (eqv? x 0)) +;;(define (positive? x) (< 0 x)) +;;(define (negative? x) (< x 0)) +;;(define (even? x) (eqv? x (* 2 (quotient x 2)))) +;;(define (odd? x) (not (even? x))) + +;;(define (max x y) (if (< x y) y x)) +;;(define (min x y) (if (< x y) x y)) + +;;(define (abs x) (if (< x 0) (- 0 x) x)) + +;;(define (remainder x y) +;; (- x (* y (quotient x y)))) + +;;(define (modulo x y) +;; (let ((q (quotient x y))) +;; (let ((r (- x (* y q)))) +;; (if (eqv? r 0) +;; 0 +;; (if (eqv? (< x 0) (< y 0)) +;; r +;; (+ r y)))))) + +;;(define (gcd x y) +;; (let ((ax (abs x))) +;; (let ((ay (abs y))) +;; (if (< ax ay) +;; (gcd-aux ax ay) +;; (gcd-aux ay ax))))) + +;;(define (gcd-aux x y) +;; (if (eqv? x 0) +;; y +;; (gcd-aux (remainder y x) x))) + +;;(define (lcm x y) +;; (if (eqv? y 0) +;; 0 +;; (let ((ax (abs x))) +;; (let ((ay (abs y))) +;; (* (quotient ax (gcd ax ay)) ay))))) + +;;(define numerator id) +;;(define (denominator x) 1) + +;;(define floor id) +;;(define ceiling id) +;;(define truncate id) +;;(define round id) + +;;(define (rationalize x y) ...) +;;(define (exp x) ...) +;;(define (log x) ...) +;;(define (sin x) ...) +;;(define (cos x) ...) +;;(define (tan x) ...) +;;(define (asin x) ...) +;;(define (acos x) ...) +;;(define (atan y . x) ...) + +;;(define (sqrt x) ...) + +;;(define (expt x y) +;; (if (eqv? y 0) +;; 1 +;; (let ((t (expt (* x x) (quotient y 2)))) +;; (if (odd? y) +;; (* x t) +;; t)))) + +;;(define (make-rectangular x y) ...) +;;(define (make-polar x y) ...) +;;(define (real-part x) ...) +;;(define (imag-part x) ...) +;;(define (magnitude x) ...) +;;(define (angle x) ...) + +;;(define (exact->inexact x) ...) +;;(define (inexact->exact x) ...) + +;; Integer to string conversion. + +(define (number->string x) + (list->string + (if (< x 0) + (cons 45 (number->string-aux (- 0 x) '())) + (number->string-aux x '())))) + +(define (number->string-aux x tail) + (let ((q (quotient x 10))) + (let ((d (+ 48 (- x (* q 10))))) + (let ((t (cons d tail))) + (if (< 0 q) + (number->string-aux q t) + t))))) + +;; String to integer conversion. + +(define (string->number str) + (let ((lst (string->list str))) + (if (null? lst) + #f + (if (eqv? (car lst) 45) + (string->number-aux (cdr lst)) + (let ((n (string->number-aux lst))) + (and n (- 0 n))))))) + +(define (string->number-aux lst) + (if (null? lst) + #f + (string->number-aux2 lst 0))) + +(define (string->number-aux2 lst n) + (if (pair? lst) + (let ((c (car lst))) + (and (< 47 c) + (< c 58) + (string->number-aux2 (cdr lst) (- (* 10 n) (- c 48))))) + n)) + +;;;---------------------------------------------------------------------------- + +;; Characters (R4RS section 6.6). + +;;(define char? integer?) + +(define char=? eqv?) +(define char? >) +(define char<=? <=) +(define char>=? >=) + +;;(define char-ci=? eqv?) +;;(define char-ci? >) +;;(define char-ci<=? <=) +;;(define char-ci>=? >=) + +;;(define (char-alphabetic? c) ...) +;;(define (char-numeric? c) ...) +;;(define (char-whitespace? c) ...) +;;(define (char-upper-case? c) ...) +;;(define (char-lower-case? c) ...) + +(define char->integer id) +(define integer->char id) + +;;(define (char-upcase c) ...) +;;(define (char-downcase c) ...) + +;;;---------------------------------------------------------------------------- + +;; Strings (R4RS section 6.7). + +(define string? (instance? string-type)) +(define (list->string lst) (rib lst (length lst) string-type)) +(define string->list field0) +(define string-length field1) +(define (string-ref str i) (list-ref (field0 str) i)) +(define (string-set! str i x) (list-set! (field0 str) i x)) + +(define (make-string k) (list->string (make-list k 32))) + +;;(define (string . args) ...) + +;;(define (string=? str1 str2) (eqv? (string-cmp str1 str2) 0)) +;;(define (string? str1 str2) (< 0 (string-cmp str1 str2))) +;;(define (string<=? str1 str2) (not (string>? str1 str2))) +;;(define (string>=? str1 str2) (not (string? string>?) +;;(define string-ci<=? string<=?) +;;(define string-ci>=? string>=?) + +;;(define (string-cmp str1 str2) +;; (string-cmp-aux (string->list str1) (string->list str2))) + +;;(define (string-cmp-aux lst1 lst2) +;; (if (pair? lst1) +;; (if (pair? lst2) +;; (let ((c1 (car lst1))) +;; (let ((c2 (car lst2))) +;; (if (< c1 c2) +;; -1 +;; (if (< c2 c1) +;; 1 +;; (string-cmp-aux (cdr lst1) (cdr lst2)))))) +;; 1) +;; (if (pair? lst2) +;; -1 +;; 0))) + +;;(define (substring str start end) +;; (substring-aux str start end '())) + +;;(define (substring-aux str start end tail) +;; (if (< start end) +;; (let ((i (- end 1))) +;; (substring-aux str start i (cons (string-ref str i) tail))) +;; (list->string tail))) + +;;(define (string-append str1 str2) +;; (list->string (append (string->list str1) +;; (string->list str2)))) + +;;(define (string-copy str) +;; (list->string (append (string->list str) '()))) + +;;(define (string-fill! str fill) +;; (field0-set! str (make-list (field1 str) fill))) + +;;;---------------------------------------------------------------------------- + +;; Vectors (R4RS section 6.8). + +(define vector? (instance? vector-type)) +(define (list->vector lst) (rib lst (length lst) vector-type)) +(define vector->list field0) +(define vector-length field1) +(define (vector-ref vect i) (list-ref (field0 vect) i)) +(define (vector-set! vect i x) (list-set! (field0 vect) i x)) + +(define (make-vector k) (list->vector (make-list k 0))) + +;;(define (vector . args) ...) + +;;(define (vector-fill! vect fill) +;; (field0-set! vect (make-list (field1 vect) fill))) + +;;;---------------------------------------------------------------------------- + +;; Control features (R4RS section 6.9). + +(define procedure? (instance? procedure-type)) +(define (make-procedure code env) (rib code env procedure-type)) +(define procedure-code field0) +(define procedure-env field1) + +;;(define (apply proc . args) ...) + +;;(define (map proc lst) +;; (if (pair? lst) +;; (cons (proc (car lst)) (map proc (cdr lst))) +;; '())) + +;;(define (for-each proc lst) +;; (if (pair? lst) +;; (begin +;; (proc (car lst)) +;; (for-each proc (cdr lst))) +;; #f)) + +;; First-class continuations. + +(define (call/cc receiver) + (let ((c (field1 (field1 (close #f))))) ;; get call/cc continuation rib + (receiver (lambda (r) + (let ((c2 (field1 (field1 (close #f))))) + (field0-set! c2 (field0 c)) ;; set "stack" field + (field2-set! c2 (field2 c)) ;; set "pc" field + r))))) ;; return to continuation + +;;;---------------------------------------------------------------------------- + +;; Input and output (R4RS section 6.10). + +;;(define (call-with-input-file string proc) ...) +;;(define (call-with-output-file string proc) ...) +;;(define (input-port? obj) ...) +;;(define (output-port? obj) ...) +;;(define (current-input-port) ...) +;;(define (current-output-port) ...) +;;(define (with-input-from-file string thunk) ...) +;;(define (with-output-to-file string thunk) ...) +;;(define (open-input-file filename) ...) +;;(define (open-output-file filename) ...) +;;(define (close-input-port port) ...) +;;(define (close-output-port port) ...) +;;(define (char-ready?) ...) +;;(define (load filename) ...) +;;(define (transcript-on filename) ...) +;;(define (transcript-off) ...) + +;; Character I/O (characters are represented with integers). + +(define eof -1) +(define (eof-object? obj) (eqv? obj eof)) + +(define empty -2) +(define buffer empty) + +(define (read-char) + (let ((c buffer)) + (if (eqv? c eof) + c + (read-char-aux + (if (eqv? c empty) + (getchar) + c))))) + +(define (read-char-aux c) + (set! buffer c) + (if (eqv? c eof) + c + (begin + (set! buffer empty) + c))) + +(define (peek-char) + (let ((c (read-char))) + (set! buffer c) + c)) +;; +;; ;;;---------------------------------------------------------------------------- +;; +;; The read procedure. + +(define (read) + (let ((c (peek-char-non-whitespace))) + (cond ((< c 0) + c) + ((eqv? c 40) ;; #\( + (read-char) ;; skip "(" + (read-list)) + ((eqv? c 35) ;; #\# + (read-char) ;; skip "#" + (let ((c (peek-char))) + (cond ((eqv? c 102) ;; #\f + (read-char) ;; skip "f" + #f) + ((eqv? c 116) ;; #\t + (read-char) ;; skip "t" + #t) + (else ;; assume it is #\( + (list->vector (read)))))) + ((eqv? c 39) ;; #\' + (read-char) ;; skip "'" + (cons 'quote (cons (read) '()))) +;; ((eqv? c 34) ;; #\" +;; (read-char) ;; skip """ +;; (list->string (read-chars '()))) + (else + (read-char) ;; skip first char + (let ((s (list->string (cons c (read-symbol))))) + (let ((n (string->number s))) + (or n + (string->symbol s)))))))) + +(define (read-list) + (let ((c (peek-char-non-whitespace))) + (if (eqv? c 41) ;; #\) + (begin + (read-char) ;; skip ")" + '()) + (let ((first (read))) + (cons first (read-list)))))) + +(define (read-symbol) + (let ((c (peek-char))) + (if (or (eqv? c 40) ;; #\( + (eqv? c 41) ;; #\) + (< c 33)) ;; whitespace or eof? + '() + (begin + (read-char) + (cons c (read-symbol)))))) + +;;(define (read-chars lst) +;; (let ((c (read-char))) +;; (cond ((eof-object? c) +;; '()) +;; ((eqv? c 34) ;; #\" +;; (reverse lst)) +;; ((eqv? c 92) ;; #\\ +;; #; ;; no support for \n in strings +;; (read-chars (cons (read-char) lst)) +;; ;#; ;; support for \n in strings +;; (let ((c2 (read-char))) +;; (read-chars (cons (if (eqv? c2 110) 10 c2) lst)))) +;; (else +;; (read-chars (cons c lst)))))) + +(define (peek-char-non-whitespace) + (let ((c (peek-char))) + (if (eof-object? c) ;; eof? + -1 + (if (< 32 c) ;; above #\space ? + (if (eqv? c 59) ;; #\; + (skip-comment) + c) + (begin + (read-char) + (peek-char-non-whitespace)))))) + +(define (skip-comment) + (let ((c (read-char))) + (if (< c 0) ;; eof? + c + (if (eqv? c 10) ;; #\newline + (peek-char-non-whitespace) + (skip-comment))))) + +;; ;;;---------------------------------------------------------------------------- +;; +;; ;; The write procedure. +;; +(define (write o) + (cond ((string? o) + (putchar 34) + (write-chars (string->list o)) + (putchar 34)) + (else + (display o)))) + +(define (display o) + (cond ((not o) + (putchar2 35 102)) ;; #f + ((eqv? o #t) + (putchar2 35 116)) ;; #t + ((null? o) + (putchar2 40 41)) ;; () + ((pair? o) + (putchar 40) ;; #\( + (write (car o)) + (write-list (cdr o)) + (putchar 41)) ;; #\) + ((symbol? o) + (display (symbol->string o))) + ((string? o) + (write-chars (string->list o))) +;; ((vector? o) +;; (putchar 35) ;; #\# +;; (write (vector->list o))) + ((procedure? o) + (putchar2 35 112)) ;; #p + (else + ;; must be a number + (display (number->string o))))) + +(define (write-list lst) + (if (pair? lst) + (begin + (putchar 32) ;; #\space + (if (pair? lst) + (begin + (write (car lst)) + (write-list (cdr lst))) + #f)) ;; writing dotted pairs is not supported + #f)) + +(define (write-chars lst escape?) + (if (pair? lst) + (let ((c (car lst))) + (putchar + (cond ((not escape?) + c) + ;#; ;; support for \n in strings + ((eqv? c 10) ;; #\newline + (putchar 92) + 110) + ((or (eqv? c 34) ;; #\" + (eqv? c 92)) ;; #\\ + (putchar 92) + c) + (else + c))) + (write-chars (cdr lst) escape?)) + #f)) + +(define (write-chars lst) + (if (pair? lst) + (let ((c (car lst))) + (putchar c) + (write-chars (cdr lst))) + #f)) + +(define (write-char c) + (putchar c)) + +(define (newline) + (putchar 10)) + +(define (putchar2 c1 c2) + (putchar c1) + (putchar c2)) + +;;;---------------------------------------------------------------------------- + +;; Compiler from Ribbit Scheme to RVM code. + +(define jump/call-op 0) +(define set-op 1) +(define get-op 2) +(define const-op 3) +(define if-op 4) + +(define feature-arity-check #f) ;; HACK + +(define (add-nb-args nb tail) + (if feature-arity-check ;; HACK + (rib const-op + nb + tail) + tail)) + +(define (comp cte expr cont) + + (cond ((symbol? expr) + (rib get-op (lookup expr cte 0) cont)) + + ((pair? expr) + (let ((first (car expr))) + (cond ((eqv? first 'quote) + (rib const-op (cadr expr) cont)) + + ((or (eqv? first 'set!) (eqv? first 'define)) + (comp cte + (caddr expr) + (gen-assign (lookup (cadr expr) cte 1) + cont))) + + ((eqv? first 'if) + (comp cte + (cadr expr) + (rib if-op + (comp cte (caddr expr) cont) + (comp cte (cadddr expr) cont)))) + + ((eqv? first 'lambda) + (let ((params (cadr expr))) + (rib const-op + (make-procedure + (rib (* (length params) 2) + 0 + ;#; ;; support for single expression in body + (comp (extend params + (cons #f + (cons #f + cte))) + (caddr expr) + tail) +;; #; ;; support for multiple expressions in body +;; (comp-begin (extend params +;; (cons #f +;; (cons #f +;; cte))) +;; (cddr expr) +;; tail) +) + '()) + (if (null? cte) + cont + (add-nb-args + 1 + (gen-call 'close cont)))))) + +;;#; ;; support for begin special form +;; ((eqv? first 'begin) +;; (comp-begin cte (cdr expr) cont)) +;; +;;#; ;; support for single armed let special form +;; ((eqv? first 'let) +;; (let ((binding (car (cadr expr)))) +;; (comp-bind cte +;; (car binding) +;; (cadr binding) +;; ;#; ;; support for single expression in body +;; (caddr expr) +;; #; ;; support for multiple expressions in body +;; (cddr expr) +;; cont))) +;; +;;#; ;; support for and special form +;; ((eqv? first 'and) +;; (comp cte +;; (if (pair? (cdr expr)) +;; (let ((second (cadr expr))) +;; (if (pair? (cddr expr)) +;; (build-if second +;; (cons 'and (cddr expr)) +;; #f) +;; second)) +;; #t) +;; cont)) +;; +;;#; ;; support for or special form +;; ((eqv? first 'or) +;; (comp cte +;; (if (pair? (cdr expr)) +;; (let ((second (cadr expr))) +;; (if (pair? (cddr expr)) +;; (list3 'let +;; (list1 (list2 '_ second)) +;; (build-if '_ +;; '_ +;; (cons 'or (cddr expr)))) +;; second)) +;; #f) +;; cont)) +;; +;;#; ;; support for cond special form +;; ((eqv? first 'cond) +;; (comp cte +;; (if (pair? (cdr expr)) +;; (if (eqv? 'else (car (cadr expr))) +;; (cons 'begin (cdr (cadr expr))) +;; (build-if (car (cadr expr)) +;; (cons 'begin (cdr (cadr expr))) +;; (cons 'cond (cddr expr)))) +;; #f) +;; cont)) + + (else + ;#; ;; support for calls with only variable in operator position + (comp-call cte + (cdr expr) + (length (cdr expr)) + (cons first cont)) +;; #; ;; support for calls with any expression in operator position +;; (let ((args (cdr expr))) +;; (if (symbol? first) +;; (comp-call cte +;; args +;; (cons first cont)) +;; (comp-bind cte +;; '_ +;; first +;; ;#; ;; support for single expression in body +;; (cons '_ args) +;; #; ;; support for multiple expressions in body +;; (cons (cons '_ args) '()) +;; cont))) +)))) + + (else + ;; self-evaluating + (rib const-op expr cont)))) + +;#; ;; support for and, or, cond special forms +;;(define (build-if a b c) (cons 'if (list3 a b c))) +;;(define (list3 a b c) (cons a (list2 b c))) +;;(define (list2 a b) (cons a (list1 b))) +;;(define (list1 a) (cons a '())) + +(define (comp-bind cte var expr body cont) + (comp cte + expr + ;#; ;; support for single expression in body + (comp (cons var cte) + body + (if (eqv? cont tail) + cont + (rib jump/call-op ;; call + 'arg2 + cont))) +;; #; ;; support for multiple expressions in body +;; (comp-begin (cons var cte) +;; body +;; (if (eqv? cont tail) +;; cont +;; (rib jump/call-op ;; call +;; 'arg2 +;; cont))) +)) + +(define (comp-begin cte exprs cont) + (comp cte + (car exprs) + (if (pair? (cdr exprs)) + (rib jump/call-op ;; call + 'arg1 + (comp-begin cte (cdr exprs) cont)) + cont))) + +(define (gen-call v cont) + (if (eqv? cont tail) + (rib jump/call-op v 0) ;; jump + (rib jump/call-op v cont))) ;; call + +(define (gen-assign v cont) + (rib set-op v (gen-noop cont))) + +(define (gen-noop cont) + (if (and (rib? cont) ;; starts with pop? + (eqv? (field0 cont) jump/call-op) ;; call? + (eqv? (field1 cont) 'arg1) + (rib? (field2 cont))) + (field2 cont) ;; remove pop + (rib const-op 0 cont))) ;; add dummy value for set! + +(define (comp-call cte exprs nb-args var-cont) + (if (pair? exprs) + (comp cte + (car exprs) + (comp-call (cons #f cte) + (cdr exprs) + nb-args + var-cont)) + (let ((var (car var-cont))) + (let ((cont (cdr var-cont))) + (let ((v (lookup var cte 0))) + (add-nb-args + nb-args + (gen-call (if (and (not (rib? v)) feature-arity-check) (+ v 1) v) cont))))))) ;; HACK + +(define (lookup var cte i) + (if (pair? cte) + (if (eqv? (car cte) var) + i + (lookup var (cdr cte) (+ i 1))) + var)) + +(define (extend vars cte) + (if (pair? vars) + (cons (car vars) (extend (cdr vars) cte)) + cte)) + +(define tail (rib jump/call-op 'id 0)) ;; jump + +(define (compile expr) ;; converts an s-expression to a procedure + (make-procedure (rib 0 0 (comp '() expr tail)) '())) + +(define (eval expr) + ((compile expr))) + +(define (repl) + (putchar2 62 32) ;; #\> and space + (let ((expr (read))) + (if (eof-object? expr) + (newline) + (begin + (write (eval expr)) + (newline) + (repl))))) + +;;;---------------------------------------------------------------------------- diff --git a/alt/scheme/ribbit/rsc.py b/alt/scheme/ribbit/rsc.py new file mode 100644 index 0000000..0dff701 --- /dev/null +++ b/alt/scheme/ribbit/rsc.py @@ -0,0 +1,87 @@ +input="R1llac/pmuj,htgnel-rotcev,?=rahc,raaadc,_,dna,po-tes,?naeloob,!llif-rotcev,trats-tni-tsnoc,rdaadc,tes,trats-tni-teg,mcl,etouq,dnapxe-dnoc,xam,oludom,esle,trats-corp-tsnoc,radddc,dnoc,rts-ot-tuptuo-htiw,po-teg,cc/llac,adbmal,rdaddc,rdadac,raadac,!tes-rotcev,trats-llac,tel,stropxe-xtc,?>rahc,rotaremun,?tsil,rddddc,roolf,trats-teg,yllamron-margorp-tixe,margorp-daer,ypoc-gnirts,enifed,yllamronba-margorp-tixe,gnirts-ekam,?evitagen,2/be,liat,gniliec,?=gnirts,certel,trats-fi,radadc,!llif-gnirts,tibbir,dmc-llehs,lbtmys,!tes-rav-labolg,?-gnirts,sfed-teser,tsil>-elbat,tros-tsil,enil-dmc,edoc-etareneg,?ddo,rahcteg,tpxe,?regetni,yrarbil-daer,dcg,elif-tupni-htiw-llac,lper,relipmoc-enilepip,htap-elbatucexe,elif-tpircs,ssenevil,sisylana-ssenevil,?=rahc,noisnetxe-htap-csr,htgnel-elbat,gnirts-ot-tuptuo-htiw,dnibnu-neg,enil-daer,!tros-tsil,elif-morf-tupni-htiw,>,lobmys-denretninu>-gnirts,?qe,rebmun>-gnirts,margorp-elipmoc,vssa,?neve,?erudecorp,elipmoc,=<,lave,?>gnirts,redniamer,elif-ot-tuptuo-htiw,?-gnirts,tsil-dnapxe,yrotcerid-htap-csr,xua-rebmun>-gnirts,xua-sisylana-ssenevil,stropxe-htiw-srpxe-pmoc,fer-tsil,raadc,repo,rebmem,rahc>-regetni,xua-gnirtsbus,2xua-rebmun>-gnirts,!tes-tsil,xua-pmc-gnirts,radac,elbat-ekam,xua-rahc-daer,edoc-erudecorp,?=gnirts,xtc-ekam,*dnib-pmoc,?tnatsnoc,!tes-elbat,radc,gnirts>-lobmys,raaac,tnemmoc-piks,tsil-etirw,tsil-daer,?ni,tsila>-stropxe,rdaac,raac,xua-tsil-ekam,rdadc,xua-esrever,raddc,xua-?tsil,evil>-stropxe,elif-morf-gnirts,liat-tsil,tixe,xua-dcg,evil-xtc,lobmys>-gnirts,?2erudecorp,tnatsnoc-dnapxe,hcae-rof,lla-daer,ydob-dnapxe,lave-dnapxe-dnoc,pmc-gnirts,elif-morf-daer,!rac-tes,1tsil,xua-gnirts>-rebmun,lobmys-esu,3tsil,nigeb-dnapxe,rotcev>-tsil,2tsil,?rotcev,rdddc,tsil-ekam,sba,enilwen,ecapsetihw-non-rahc-keep,regetni>-rahc,lobmys-daer,poon-neg,?gnirts,lobmys-denretninu>-rts,tsil>-rotcev,fer-elbat,gnirtsbus,ngissa-neg,tes-etc-xtc,cossa,!tes-2dleif,gnirts>-rebmun,enod-ydob-dnapxe,2rahctup,rdddac,?tcejbo-foe,llac-neg,llac-pmoc,?ecnatsni,daer,fer-gnirts,srahc-daer,etanetacnoc-gnirts,rts>-lobmys,rahc-keep,pp,fi-dliub,?evil,dnetxe,erudecorp-ekam,sesualc-dnapxe-dnoc-dnapxe,dnib-pmoc,etirw,*nigeb-dnapxe,!tes-1dleif,dnpo,etc-xtc,gnirts>-maerts,rorre,?llun,txen,qssa,!rdc-tes,tneitouq,?bir,pukool,evil-dda,srahc-etirw,nigeb-pmoc,=>,esrever,htgnel-gnirts,dneppa,vmem,rddac,=,gnirts>-tsil,!tes-0dleif,xeh-daer,tsil>-gnirts,?lobmys,htgnel,rahctup,*,2dleif,rpxe-dnapxe,yalpsid,hguorht-epip,ecalper-gnirts,setyb-ot-edoc-mvr,rid-toor,?lauqe,pam,dnapxe-htap,rahc-daer,0dleif,1dleif,rddc,pmoc,ton,<,+,-,rdac,esolc,dneppa-gnirts,?riap,rac,?vqe,1gra,2gra,rdc,di,snoc,lin,eurt,eslaf,bir;8U0!U08BYU9YTMYSaZPZ#h-_f7$IlfA^[$G7/fJldb7'Il^~YU+ZB`h1ZBJ`dh/70>>h.ZPh.gh3h4_^Jh/c~Kk^zi$~YTHZ#gJf_|i$Z#aZ#_|!?9@`SYI_G9KYS)^z{!U9(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBi$(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBYBiX9BYBZ#^BYBiY+~Z%ldFiY*(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBi$(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBYBiX9BYBZ#^BYBiY+~Z%ldFYSEe~e_YSERUFFFdiXJbiX$(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBi$(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBYBiX9BYBZ#^BYBiY+~Z%ldFiY*(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBi$(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^LgAWiX8iX.WViXHaViXA_WViWNaViX*_Wa_`iXO(^YA_RUFFiXBeiXI(^^~ATiY&e(^YA_RUFFiXBeiXI(^^~A^~^Lg^~TiX0cBYBiX9BYBZ#^BYBiY+~Z%ldFYSEe~e_iX&~TiX(aYTA__@cDb}(!SE8U2G8U&i$^z^z!VO8PYS8`8PYS<~TiY-`YU4^{!U48S8^8S8RRUiY%FiXM^~TiXFYU*^z!S88U$iS<^z!S<'YS<^(i&~ZG^ZCy!>8T=AYU/8T=A^~^YU.y!U>8SaG8LZ$YI^Z?^zZ1XBi&IYG`YGeX?i&hR%7'@^~i$/FFZ1aiX?Z@iX>SaG8LZ$YI^Z?^zZ1XBi&IYG`YGeX?i&hR%7'@^~YT,iX6Z?D^~E^zi$70>h-a@eJlcBYKd^@aYS%iX,7.e@ca~^Z-hR%^D^D_~E_|i$7.e@ca70>h-a@eJlcBYKd^@a@^~^Z-hM^D^D_~E_|i$70>h-`@eJlcBYKd`YS%iWP70>h-`@eJlcBYKd`@^~^Z-hH_@_D^D_~i$7&ca_A^[$G7&cg_A^[$G7$aA^[$G/FFZ1aiX?Z@iX>SaG8LZ$YI^Z?^zZ1XBi&IYG`YGeX?i&hR%7'@^~i$/FFZ1aiX?Z@iX>SaG8LZ$YI^Z?^zZ1XBi&IYG`YGeX?i&hR%7'@^~YT,iX6Z?D^~E^zi$70>h-a@eJlcBYKd^@aYS%iX,7.e@ca~^Z-hR%^D^D_~E_|i$7.e@ca70>h-a@eJlcBYKd^@a@^~^Z-hM^D^D_~E_|i$70>h-`@eJlcBYKd`YS%iWP70>h-`@eJlcBYKd`@^~^Z-hH_@_D^D_~KiV4^~E_|i$YU;YUciUMZ3`Z._~CiV.^7+AZ0_iX@7+AX/c^~YS?^7+AX3dX4_iV6~YH^7+AX4d_iW@7+A>cJ_iV,~Kv.^~X-^Z3`Z._~CiWH^7+AZ0_iXG7+AX3dX4_iVE~YH^7+AX4d_iW=7+A>cJ_iW#~Ku^~X-^Z3`Z._~CiW2^7+AZ0_iXN7+AX3dX4_iV&~YH^7+AX4d_iVA~X-^Z3`Z._~CiWC^7+AZ0_iY(7+AAX4e_iVB7+AA>dJ_iW+~KiV4^X3^~YH^7+AX4d_iWJ~X-^Z3`Z._90_iY)73d_iV<'cJ_k~Kv7^X2^~YH^73c_iUP~X,^Z3_~CZ.`k~CiUA^YT7^~Z*^{[&G8U5^z['G7,X4d`JoiW67,>cJ`iW6~Ko_Z._P^YT-^{[(G72e_`(^~YMk`>bJiVI^72e_`(^~YMk`>b^~CcaIYEiVI__Z+iVI^|[)G7.a`^{[*G'``'@`Jl`~YMJliVI^D^X/a_|[+G'X0b`^|[,G89^YS#i$_h>z[-G(i$76n^~CiWC_77l^BZ4_aBYKiW2aX8^76m^~AAi$77l^BZ4_aBYKiW2aX8^76m^~AAZ%k`~X-`77l^BZ4_aBYKiW2aX8^76m^~AA^~^YS?_77l^BZ4_aBYKiW2aX8^76m^~A^~^YH^~CiWH_76l^~CiW2_76k^~CiUA_73^~CiV._Z3_YT7^z[.G(i$71Z.^BX0^~Z*^z[/G(i$75^~YS?^(i$9LJYD`l^~YMm`94JO`l^~YMl`8KJP`l^~YMk`AA^BYT(`ahA:kkk(i$9LJYD`l^~YMm`94JO`l^~YMl`8KJP`l^~YMk`A^~^YS#i$_h?~YH^{[0G(^BX4`^{[1G71Z.YT-^z[2G(^BX4k^z[3G(^BX6n^BX?_`[9Jlh8YS@FZKh8iX#1^~^Z-h>^(w&~Ci&^(w%~Ci%^(w$~Ci$^z[4k[5G9LX=X;YD_^P^z[6G7%`h>A^[$G(_7-XE:gbiWC_@bN`H_D^D^~E^{i$z[7G#X>bYD`O_P^(_~Ck^{[8G7%a@iX-A^[$G(_7)AAb7)AAX-c_~i$7)AAb7)AAX-c_~AAKYDak7)AAb7)AAX-c_~AA^~^KO`k7)AAb7)AAX-c_~A^~^KP_k~^YS#i$_hFDD_@^~E^{i$[$G#::::h.XBngiWCX@kw#iUAliWHkiWHHZ-iX-_iWH{i$z[9G(`[@>h@>>`a_X?k^{[:G90_iX/7@XA::fX;kw#iUAoiWHYG_^YS$^~YS/^7@XA::fX;kw#iUAniWHYG_^SYI_iS)~YS&^7?X@::eX:kw#iUAkiWH@_D^~E^#a_iWH#::eX:kw2iUAIbkiWHkiWH~Kk^~X*^#aX6m_iWH~YH^#bX7l_iW2X7^~AZ-h@_90_iX/7@XA::fX;kw#iUAoiWHYG_^YS$^~YS/^7@XA::fX;kw#iUAniWHYG_^SYI_iS)~YS&^7?X@::eX:kw#iUAkiWH@_D^~E^#a_iWH#::eX:kw2iUAIbkiWHkiWH~Kk^~X*^#aX6m_iWH~YH^#bX7l_iW2X7^~A^~^YOiX%^{[;i&[@``^~^Z;h1_Bi$(i$70i$d_BZ,>@``^~^Z;h1_BX/_~h17/i$c^~YSOc_YN`H_~CwV%^7-^H_~CwW;^D^~E^7,^(i$~YSO`^~YH^|[&G(i$8S=YS$_c~YS/^7)@^BX)D^~E^7*^~YH^z['G(k[,Z(g^zi$i$i$i$|!SO5CZ)ka_^{!T)(i$(i$(i$,DH_wW;~EH^~Z/N^~E@^z];(i$9;@a_(^~CD__D_~E_{]('a^>i&^(_~Z;`^{!U=7$_A^[$G(i$7'@^BZ,i&D^~E^zi$z!SF(i&'YSF@_>i&DD^~E^z!T;7$Z(Z(Z(Z(YSFAi&7$Z(Z(Z(Z(YSFA^~^dw(w0w*w+A^[$G7(^(_~C`^YU-Le_bBYU=^zi${!U,(^8T;a_~ZM_wUHYT;i&^{!T>(i&'YT>@_YCD^~E^z]8(_98a@_95a@^~AYS:D_98a@_95a@^~A^~^CD_wW7D^~E^{!S:,wUJ^5LYOS@`iS:i%~i$,wUJ^5LYOS@`iS:i%~CwV5D^~E^5YOS@`iS:i$~i$,wUJ^5LYOS@`iS:i%~i$,wUJ^5LYOS@`iS:i%~CwV5D^~E^5YOS@`iS:i$~CwWDD^~E^5YS:H^~i$,wUJ^5LYOS@`iS:i%~i$,wUJ^5LYOS@`iS:i%~CwV5D^~E^5YOS@`iS:i$~i$,wUJ^5LYOS@`iS:i%~i$,wUJ^5LYOS@`iS:i%~CwV5D^~E^5YOS@`iS:i$~CwWDD^~E^5YS:H^~Cw5D^~E^z]5(_'_YC_98_@_~i$'_YC_98_@_~CwW:D_~E_95_@_~i$'_YC_98_@_~i$'_YC_98_@_~CwW:D_~E_95_@_~CwV'D_~E_Z5a@_D^~E^{!S28S>k-^'_wV'~E@^~E^Z5i&^z]J8S2_8C>>aZ$_wUN~E^{!S;7%i&_A^[$G9JiX<_9J``7+>c>Na_@`7+>c>>i&>>Nc@awW0D_@`~E^H^~i$9J``7+>c>Na_@`7+>c>>i&>>Nc@awW0D_@`~E^H^~i$9J``7+>c>Na_@`7+>c>>i&>>Nc@awW0D_@`~E^H^~E@^~CD_wVM~E^D^~E^{i$z!S>'>i&_wW;z!C8S>^8T>_8Ci$8C>>>>i&>NcwW4>@HbwV'DH`wU?8C>@H`wV'~CDH`wW7~E@_~CwW4^8Ci$8CH_8C>>>i&>>>>i&>NewV5wWEwWEwU?>i&>>i&HawWEwW*~EN_~E@_~CwV5^8Ci%8CH_8C>>>>i&i$>NbwWDH`wU?~EN_~E@_~CwWD^'>>i&YCYNb_wV%'>>i&YC>>Nd@awW0D_wV%~E^H_~CwVM^8S2@_~CwV'^8C>>YPNcSaG'>>i&H`D_wV%zS`G'>i&i$D^zwW*H_~CwUN^8C>@a8C>>>i&>>Nd@awV)>i&D_~i$8C>@a8C>>>i&>>Nd@awV)>i&D_~E@_~E_wW*H_~CwV)^8S;Na'>>i&YS;NcS`G'>i&YCH_D^zwW*~E^^8CA>S`i1>>>i&a>i&>>i&>>YS.eSbi-wW0`wUNYN`~YH^H_~CwW*^'>>i&YS;Nb_wW0H_~CwW0^'>>>i&i$'>>>i&YCZHb~EYS.bYCYNaYCH`wU?~CwU?^'>>i&YCYNb_wV%H_~CwV%^8S>H_~CwW;^D^~E^(^~YH^z!TM(^Bi$(^BZ=@^BYBiX3~Z%nbBi$(^Bi$(^BZ=@^BYBiX3~Z%nbBZ=D^BYBiXK~Z%mbYT:YSN_>i&i$(^Bi$(^BZ=@^BYBiX3~Z%nbBi$(^Bi$(^BZ=@^BYBiX3~Z%nbBZ=D^BYBiXK~Z%mbYT:YSN__~E_@_D^YT@_{!T:'ASaG'_^D^z'A^~^bZ9i&:MiVHbYT+Ai&'ASaG'_^D^z'A^~^bZ9i&:MiVHbYT+A^~^eai&kkYU,a^YS2^{!SN(^8<_G'H_D^'_^~YH^z~E^z!T@7&i$i&_A^[$G'aZ$_7,c>b_@_7,YPAi&7,YPA^~^d@`a@_~i$7,c>b_@_7,YPAi&7,YPA^~^d@`a@_~CwV>D^~E^D^~E^|i$z!VH:kw(iUA]:(_'Z:a@_D^~E^{])(^9)Jlb@`^(`~C_D_~E_|]E7&^6ZEd@bZN>Z2bi$`D`^~E_|]&6b6:Z&f@dbYS4w+aiUA~E@aD`^|!S4(_BYTBZ(YSA``^{!U'#aYS4w*_iUA(_~CiVH_{!T*9&eca6YT*YU'h2gh/ZN>Z2h/eh-@f@dZN>Z2di$b_`DaD_~E_})]78T*geab`^}(!S'#`kiWH8D^~i$#`kiWH8D^~i$#`kiWH8D^~i$#`kiWH8D^~Z*YD^~Cw+O^~CiUAP^~Z*^z]O#YS'a_iWC{]F#a_iUA#k_iUA~CiVH_{!6#b`iWH97f>i&>bwWE>i&aiXD`9EG9Fh.^Z)kZ2_dz_`~YH_@`97gaSbi1Sai-aNaH`~CwW*^9&c@a_~CwV'^#ZFeYS4w0b#d~Z/Z2bZ9i&:Z&iVHNeZNZ:>>Z2gi$i$bckYG_iWHH`~CwW0^6`Hdb:a_iV.MdYNb`McZHa_~CwU?^6ZOg``b8S'e6ZOh-aac8S'e~YT)^~^Z;YSAc`~Ca^Z)lZ2b_YNaH`~CwV%^#cHaiWH~CwW;^D_~E_#d`iW2#dHH_iWH~i$#d`iW2#dHH_iWH~YT)^~^Z;YSA``Z)kZ2__~YH_|!TB8S7`O^{]N#YD`O__{!W)8D^z!SA-O^z]289^z!T+#b>i&`^|].8D^z]388^z!T789^z!S?iTJ!V.wU?!WHwV(!W2wV*!WCwW>!UAwWM]@(iY'7%YIDa@`A^[$G8L_7)YPYPbeYID_@^~E^{i$YI`Z$^~E^{!U/(iXPy!U.-YU:y!U;8U%YPi&`^{!U%7(_c(i&~YM_kYGb[$G(^BZ,b_(^BZ,a_7/f@dc`BZ,ca7/@fdd`BZ,da~X0`^DbD`~Ea~E`}'[%G(_(^7-d@ba`7-@dbba~X.`^D`D^~E_~E^{[&G7%a_A^[$G71_X2eeX1Ief_7)@`Il^~YU#k^{i$Z+m_(^BZ,i&^~YMl_{i$i$i${]=8S+BZ6^z!U&7$i&A^[$G7(>`^8LZ$_~ACf_7(>`^8LZ$_~A^~^ZG^Qbzi${!;/F`iX+_/__~YMvR$YS)ZBIlZ#`_(^~YMkZ#_{!T=7$IlZ#_A^[$G7'Il^9PJl`kb~YMvR$YS)ZB_b(iWO~Kk^zi$z!U*7$IlZ#_A^[$G7'Il^9PZ#d_b~YMvR#YS)ZB_b(iX1~Kk^zi$z]?8S#YT&`_iV9z!S%(^BYT(b_iV9YS@^FZKYU)iV9iX:z!V9YT/!U<-^z!U)8GD^z!T(8S7>Da>ca_9,b^~^ZMD__|!S#(a)^~^ZMD__|!T/'i&i&y!W38U(^z!VL8SCly!VP8SCky!U:'i&iY,y!A(_BYBiX'BYB^BYBiXLBYBiY$BYBiX={!VC(i$z!UI(i$z]08SClBYS+BZ6_BYBiXEBYB^{!U18U1BYS+BZ6YTG^8S+~ZG^ZCBZIvCvR3y!TG7#YTI^z!TI99i&:MiVHai&kkz!VH:kw(iUA]:(_'Z:a@_D^~E^{])(^9)Jlb@`^(`~C_D_~E_|]E9F`^Z)ka_@aD`6ZEd@b>ai$D`^~E_|!S'#`kiWH8D^~i$#`kiWH8D^~i$#`kiWH8D^~i$#`kiWH8D^~Z*YD^~Cw+O^~CiUAP^~Z*^z]O#YS'a_iWC{]F#a_iUA#k_iUA~CiVH_{]&6b6:Z&f@dbw+iUA~E@aD`^|]76Z&:h-w*iUA6Z&f~CiVHfd>aaa^}(!S6'i&^z!S0'YS6`^{!S3'YS0b`^|]<'YS3ca_wU?|!6#b`iWH97f>i&>bwWEawWE`9E>ea_`~YH_@`6ci$6cZ<>NdwW4>@HcwV'DHa6c>@HbwV'~CDHbwW7~E@a_~CwW4^6ci$6cA^6cAYS3Z<>NgwV5wWEwWEYS6YS0`wWEwW*~ENbHa~E@a_~CwV5^6ci%6cA^6cAZNdwWD^~ENbHa~E@a_~CwWD^97fNdH`D_`DH`~CwW*^9&c@a_~CwV'^#ZFew0#d~Z/bZ9i&:Z&iVHNeZ:>>fi$i$akYG_iWHH`~CwW0^6:MgZHecMfYNdbiV.Ha_~CwU?^6ZOdZ)lbHbYNa_~ACwVM_97f>i&>bwWEawWE`9E>ea_`~YH_@`6ci$6cZ<>NdwW4>@HcwV'DHa6c>@HbwV'~CDHbwW7~E@a_~CwW4^6ci$6cA^6cAYS3Z<>NgwV5wWEwWEYS6YS0`wWEwW*~ENbHa~E@a_~CwV5^6ci%6cA^6cAZNdwWD^~ENbHa~E@a_~CwWD^97fNdH`D_`DH`~CwW*^9&c@a_~CwV'^#ZFew0#d~Z/bZ9i&:Z&iVHNeZ:>>fi$i$akYG_iWHH`~CwW0^6:MgZHecMfYNdbiV.Ha_~CwU?^6ZOdZ)lbHbYNa_~A^~^CwV%^#cHaiWH~CwW;^D_~E_#bZ)k``iW2~YH_|!V.o!WHn!W2m!WCl!UAk]I8F_BYF^{!S+8Fuy!UEiF]'(i$9'a@_BYF^9'a@_BYF^BYFvS#~ACvS#_9'a@_BYF^9'a@_BYF^BYFvS#~A^~^CvE^9'a@_BYFvS;BYFvS#~Ct^9'a@_BYFvS9BYFvS#~Cv0^9'a@_BYFvS5BYFvS#~Cu^9'a@_BYF^~L`D^~E^{!T#(i$(i$8T#@^BZ6D^~E^BYFvC~E^z!B8BZK^9IvS7vF~YTJ^96YS$^BYFvF~YS/^9'i$YI^~YS&^8BYT&^~YH^8FvLBYT#@^BZ6D^BYFvK~E^9IvLvK~Z/^9IvS;vF~Ci%^9IvS-vF~L^z]68B^8FvEBZ'i%YI^BYFvE~YS&^z!T$8T$8S*~Cu^(^~Kk^Qy!S*8S*BQ(^8T$~CvR0^~K_vC(iX5~ZG^Z>y]A9A>`^9A>a^9A>at~CvS;^9A>av0~CvS9^9A>au~CvS5^Q~CvS#^9$_~CvE^(i&~ZG^Qz!S('YS(^BQ(i&~AAKvD`'YS(^BQ(i&~AA^~^CvL_'YS(^BQ(i&~A^~^CvK^Z>y!SP'YSP^ZC(i&BQ~CvL^YS*y!J(_8JIIvRL_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~KvS.^~K_vS'8JIIvR,_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~KvS.^~K_vS'8JIIvR,_YE`v3BQ~KvR<^~K_vR58JIIvR%_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~KvS.^~K_vS'8JIIvR,_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~i$(_8JIIvRL_YE`v3BQ~KvS.^~K_vS'8JIIvR,_YE`v3BQ~KvR<^~K_vR58JIIvR%_YE`v3BQ~KvR/^~K_vR$Z>z]C8S@`(^~^^YTN^YL>YS(^BQ8LZAi&BQ~CvE^'>i&ZCwW;BQ~CvJ^8S1ZC2YJkk8JkBQ~CvP^Z>BQ~ACvRM_8S1ZC2YJkk8JkBQ~CvP^Z>BQ~A^~^CvS?^(i%BQ~CvS;^(i$BQ~CvS-^Z>BQ~CvF^8SPBQ~CvK^(^~Kk^YS*y]>(^!V3^Qy!T.(^!V3iX4(^~CiX5^!V3^z!:8T.^8T.YU7~CiX4^(^~CiX5^iV3y!V3iX4]G,iX5^z!W17%G(_BZLYDc^BYKPc^OOGi$zOOGi$z!S=(i$8S=@`^BX$D_~E_{!<(i&'S@a_X$D_~E_{!V#i8!T-i9]9#l`^{!TJZDl!WA8KYS-aO_^{!V28S1YS-k^z!W,8T2b`P^|!U@8T9`P^{!WLi8!S$i9!S1#oYG_^z!S/ZDo!UK8KYS-aO_^{!VN8LYPi&YI^z!/8LYPYI`YI^{!T48La8T4>fZBbb`a_Il`~Ka_}']P8T4i&b`^|!T1(k(iX5~E_(l8T1@b@`(l~K`^(iX5~K__D`D^~E_~E^{!S98T1YI`YI^{!UO5YTC`^{!WK5YTF`^{!TF4YS9a_k{!TC4kYS9`^{!T,,kYS9`^{!VK8LYS-vC^z!V18T2b`P^|]B8T9`P^{]#i8!Ii9!L#nYG_^z!S&ZDn!T5i(!S)i(!WGj%!VFiTH!W(iU#!UFi4!U+i,!T3(_(i$(i$8T3IIvR%`YEbu@_~KvR/^~K_vR$D^~E^{!T<8T3k^(i$~Z/^z!TN(i$2_k~^YT<^8T<@^~CvPD^(i$~Z/^YI^z!S5(^8S5_`~Kak>b^JIYEu``vR%Z+u^{]K8LYS5i&^8L>YS5i&I`kvP~Kk^z!U6(^8E__~YU8`YU6Z+m`YE_^(l~Ck_{!VDi(!V;i(!VGi(!W$i(!V?(lz!W'i(!W<8E_Z+YU3``_YS,`YS,^(k~Ck_{!SB8SB_YTE__(_~Ck^{!U38SB`^8SB__~K__YS,`YS,^{!W83b^(^~CKkbKk`(k~Ck^IYE`a_Z+`^{!TE2YEZ+b``^{!S,(^2_k~Kk^z!V@(_(^~K`^{!W9(^(_~K`^{!U85YTK^z!TK,YEZ+m`m^z!VJ4k^z!UD4_kz!UC,k^z]%5K`^{!TH5K__{!U#4__{!Mi,!U55Z*^zBZ4ki#!UHOi#!T?(^!UH>iUH^YTP^8T?Oa_(^~T`O^P_~E_{!S@8T?iUH^z!UGiK!V/i9!T&i8!TP#m_i$z!HZDm!SK(`8SK>ca`Il^~K_k|!S-8SKi&`^{]M(i$9M@a_(^~TD__D_~E_{]-iTL!TL(i$8TL@a_(^~CD__D_~E_{!T6(i$8T6@`^(_~TD`^~E_{!V0iO!O(i$8O@`^(_~CD`^~E_{!SD(^8SDIl`@^~K`k{!T28S7aYSD`^|!T9-YSD`^{!SI(_8SI>aD_@^~E^{]$8SIi&^z!P(_'YPa@_D^~E^{!G(k3YG@_l~E^z!SG9/^9/^8SG@a@^~E^(i$~YTOa^@^~E^{!W&8SG_^z]/,i&^z!W%8S.O^z!W58S.P^z!W/8SHO^z!V-8SHP^z!V+8SJO^z!UL8SJP^z!W?8T8O^z!WF8T8P^z!V88NP^z!W.8T0O^z!W-8T0P^z!UB8SMO^z!V:8SMP^z!V78T%O^z!WI8T%P^z!S.87O^z!SH87P^z!SJ8T'O^z!T88T'P^z!T01P^z!SM8SLO^z!T%8SLP^z!T'88P^z!SL89P^z]H8NO^z!N1O^z!788O^z!189O^z],j4!S7iK!)i8!-i9!'#k`^{!.ZDk!=(i$(i$(i$(i$8=PaP_~TOaO_~TYDaYD_~Z*`(i$~CpYD_~Z*_(^~^C`^{!TOi,!WB5_(^~^Ci%^z!5,i$^z]D0(i$,bYD^~Z*^zz!XC:nn:k:k:ki&vS4vS=vS9!X;:nl:ki&vP!Y#:nki&!XJ:np:k:k:k:k:ki&vR#vS4vS=vS9vR$!X$:np:k:k:k:k:ki&vR$vS;vS:vS6vS/!Y*:nki&!X8:nv::k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:ki&vS4vS(vS9vS.vS6vS9vS7vCvS,vS/vS;vCvS-vS6vCvS,vS+vS6vS*vCvRBvRKvRG!X.:nv>:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:ki&vDvRDvRAvRAvR:vR=vCvS:vS;vS5vS0vS9vS7vCvS;vS(vS/vS;vCvS,vS+vS6vS*vCvRBvRKvRG!XH:nl:ki&vO!XA:nl:ki&vO!WN:nl:ki&vC!X*:nl:ki&vC!XO:nvR$:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:ki&vS@vR+vS=vS2vS3vR/vJvDvS4vS2vS3vR/vKvDvR2vRGvS=vR3vR4vR/vRGvS=vR3vR4vR6vRGvS=vR3vR4vR6vRGvS=vR3vR4vR9vRGvS=vR3vR4vR9vS=vR3vR4vS:nl:ki&vO!X6:nki&!X,:nki&!WP:nki&!X):k:k:k:ki&w&w%w$w#!X2:nv/:k:k:k:k:k:k:k:k:k:k:k:ki&vS+vS,vS;vS*vS,vS7vS?vS,vCvS)vS0vS9!X7:nu:k:k:k:k:k:k:k:k:k:ki&vS7vS6vCvS5vS>vS6vS5vS2vS5vSvCvS;vS/vS.vS0vS4vCvSvCvS,vS+vS6vS*vCvS+vS,vS;vS(vS9vS,vS5vS,vS.vCvS,vS/vS;vCvS6vS:vCvMvMvM!X=:nvR/:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:k:ki&uvS4vS,vS;vS:vS@vS:vCvS,vS4vS,vS/vS*vRHvCvS:vS0vS/vS;vCvS/vS;vS0vS>vCvS+vS,vS;vS9vS6vS7vS7vS0:b-=1;h=[[q,[r,0,3],2],h,0] +t=r +b=0 +while 1: + e=I() + if e==44:h=[[q,[t,b,3],2],h,0];t=r;b=0 + else: + if e==59:break + t=[e,t,0];b+=1 +h=[[q,[t,b,3],2],h,0] +J=lambda b:C(h,b)[0] +while 1: + a=K();b=a;p=0;k=0 + while 1: + p=[20,30,0,10,11,4][k] + if b<=2+p:break + b-=p+3;k+=1 + if a>90:b=g() + else: + if k==0:d=[0,d,0];k+=1 + b=x(0)if b==p else J(x(b-p-1))if b>=p else J(b)if k<3 else b + if 4= TRACE_COARSE + and (computer.pc == interp_loop_addr or computer.pc == halt_loop_addr)): + if last_traced_exec is None: + print(f"{ribs:,d}; cycle {cycles:,d}:") + else: + print(f"{ribs:,d}; cycle {cycles:,d} (+{cycles - last_traced_exec:,d}):") + last_traced_exec = cycles + ribs += 1 + + print(f" stack ({inspector.show_addr(inspector.peek(stack_loc))}): {inspector.show_stack()}") + + next_rib = unsigned(inspector.peek(next_rib_loc)) + current_ribs = (next_rib - big.HEAP_BASE)//3 + max_ribs = (big.HEAP_TOP - big.HEAP_BASE)//3 + print(f" heap: {current_ribs:3,d} ({100*current_ribs/max_ribs:0.1f}%)") + print(f" PC: {inspector.show_addr(inspector.peek(pc_loc))}") + + # # HACK? + # print(f" symbols (n..0): ({inspector.show_addr(inspector.peek(symbol_table_loc))}) {inspector.show_stack(inspector.peek(symbol_table_loc))}") + # print(f" ribs:") + # for addr in range(big.HEAP_BASE, unsigned(inspector.peek(next_rib_loc)), 3): + # print(f" @{addr}; {inspector.show_obj(addr, deep=False)}") + + print(f" {inspector.show_instr(inspector.peek(pc_loc))}") + elif trace_level >= TRACE_FINE and computer.pc in symbols_by_addr and computer.pc != halt_loop_addr: + print(f"{cycles:3,d}: ({symbols_by_addr[computer.pc]})") + elif trace_level >= TRACE_ALL: + print(f"{cycles:3,d}: {computer.pc}") + + def meters(computer, cycles): + inspector = Inspector(computer, symbols, stack_loc) + next_rib = unsigned(inspector.peek(next_rib_loc)) + current_ribs = (next_rib - big.HEAP_BASE)//3 + max_ribs = (big.HEAP_TOP - big.HEAP_BASE)//3 + return { + f"mem: {100*current_ribs/max_ribs:0.1f}%" + } + + + big.run(program=instrs, + simulator=simulator, + name="Scheme", + halt_addr=halt_loop_addr, + trace=trace if trace_level > TRACE_NONE else None, + verbose_tty=verbose_tty, + meters=meters) + + +def compile(src): + import subprocess + result = subprocess.run(["python", "alt/scheme/ribbit/rsc.py"], + text=True, + input=src, + capture_output=True) + if result.returncode != 0: + raise Exception(f"Compiler failed: {result.stdout}") + return result.stdout + + +def decode(input, asm): + """Decode the compiler's output as data in ROM. + + The strings which are the names of symbols are written as ribs in the ROM. + A table of addresses of those strings is written to ROM; it will be read by the runtime + during initialization when the storage for symbols is allocated in RAM. + + Each encoded instruction is written as a rib in the ROM. When an instruction references + a symbol, the reference is resolved to the address in RAM where that symbol is expected + to be allocated at runtime. + + See https://github.com/udem-dlteam/ribbit/blob/dev/src/host/py/rvm.py#L126 + """ + + asm.comment("=== Data ===") + + # for comprehension: + pos = -1 + def get_byte(): + nonlocal pos + pos += 1 + return ord(input[pos]) + + def get_code(): + """Decoded value of the next single character.""" + x = get_byte() - 35 + return 57 if x < 0 else x + + def get_int(n): + """Decoded value of sequence of characters""" + x = get_code() + if x < 46: + return 46*n + x + else: + return get_int(46*n + x-46) + + + def emit_rib(lbl, x, y, z, comment=None): + asm.label(lbl) + if comment: + asm.comment(comment) + asm.instr(x) + asm.instr(y) + asm.instr(z) + + def emit_pair(lbl, car, cdr, comment): emit_rib(lbl, car, cdr, "#0", comment) + def emit_string(lbl, chars, count: int, comment): emit_rib(lbl, chars, f"#{count}", "#3", comment) + + + # Strings for the symbol table, as constant ribs directly in the ROM: + + # One empty string that can be shared: + emit_string("rib_string_empty", "@rib_nil", 0, '""') + + # First byte(s): number of symbols without names + n = get_int(0) + sym_names = n*[("rib_string_empty", "")] + + asm.blank() + + accum = "@rib_nil" + acc_str = "" + idx = 0 + while True: + c = get_byte() + if c == ord(",") or c == ord(";"): + if acc_str == "": + lbl = "rib_string_empty" + else: + lbl = f"rib_string_{idx}" + emit_string(lbl, accum, len(acc_str), f'"{acc_str}"') + idx += 1 + sym_names.insert(0, (lbl, acc_str)) + + accum = "@rib_nil" + acc_str = "" + + if c == ord(";"): + break + else: + lbl = asm.next_label("char") + emit_pair(lbl, f"#{hex(c)}", accum, f"'{chr(c)}'") + accum = f"@{lbl}" + acc_str = chr(c) + acc_str + + asm.blank() + + # Exactly one primitive proc rib is pre-defined: `rib` + # As a completely over-the-top hack, the location of the symbol table in RAM is stashed in + # the otherwise-unused second field. + # Note: can't emit this rib until the size of the symbol_table is known, hence the odd sequence. + asm.label("rib_rib") + asm.instr("#0") + asm.comment("Location of symtbl:") + # asm.instr(symbol_ref(0)[0]) + asm.instr(f"#{big.HEAP_BASE + 6*(len(sym_names)) - 3}") + asm.instr("#1") + asm.blank() + + # Three constants the runtime can refer to by name: + def special(name): + asm.label(name) + asm.instr("#0") + asm.instr("#0") + asm.instr("#5") + special("rib_false") + special("rib_true") + special("rib_nil") + asm.blank() + + # TODO: move this table elsewhere, so the ribs for strings and instructions form a monolithic + # block of address space? + asm.comment("Table of pointers to symbol name and initial value ribs in ROM:") + asm.label("symbol_names_start") + sym_names_and_values = list(zip( + sym_names, + ["rib_rib", "rib_false", "rib_true", "rib_nil"] + ["rib_false"]*(len(sym_names)-4))) + for i in reversed(range(len(sym_names_and_values))): + (lbl, s), val = sym_names_and_values[i] + asm.comment(f'{i}: "{s}"') + asm.instr(f"@{lbl}") + asm.instr(f"@{val}") + asm.label("symbol_names_end") + + asm.blank() + + # Primordial continuation: + # x (stack) = [] + # y (proc) = 0 + # z (instr) = halt + emit_rib("rib_outer_cont", "@rib_nil", "#0", "@instr_halt", "Bottom of stack: continuation to halt") + + asm.blank() + + # Decode RVM instructions: + + asm.comment("Instructions:") + + emit_rib("instr_halt", "#5", "#0", "#0", "halt (secret opcode)") + + stack = None + def pop(): + nonlocal stack + x, stack = stack + return x + def push(x): + nonlocal stack + stack = (x, stack) + + def symbol_ref(idx): + """Statically resolve a reference to the symbol table, to an address in RAM where that + rib will be allocated during initialization. + + The table is written from the end, and each entry is made of of two ribs, the `symbol` + and a `pair`. + """ + name = sym_names[idx][1] + description = f'"{name}"({idx})' + return f"#{big.HEAP_BASE + 6*(len(sym_names) - idx - 1)}", description + + def emit_instr(op, arg, next, sym): + lbl = asm.next_label("instr") + + asm.label(lbl) + + if sym is not None: + target = sym + else: + target = arg + + if op == 0 and next == "#0": + asm.comment(f"jump {target} ") + elif op == 0: + asm.comment(f"call {target} -> {next}") + elif op == 1: + asm.comment(f"set {target} -> {next}") + elif op == 2: + asm.comment(f"get {target} -> {next}") + elif op == 3: + asm.comment(f"const {target} -> {next}") + elif op == 4: + asm.comment(f"if -> {arg} else {next}") + else: + raise Exception(f"Unknown op: {op} ({arg}, {next})") + + asm.instr(f"#{op}") + asm.instr(arg) + asm.instr(next) + + return lbl + + # For each encoded instruction, emit three words of data into the ROM: + # - references to symbols are statically resolved to addresses in *RAM*, + # where the references + + # TODO: reverse the instruction stream so it reads *forward* in the commented assembly listing? + + # FIXME: this horribleness is ripped off from https://github.com/udem-dlteam/ribbit/blob/dev/src/host/py/rvm.py directly + # What part of this happens at runtime? + while True: + if pos >= len(input)-1: break # TEMP + + x = get_code() + n = x + d = 0 + op = 0 + + sym = None + + while True: + d = [20, 30, 0, 10, 11, 4][op] + if n <= 2+d: break + n -= d+3; op += 1 + + if x > 90: + n = pop() + else: + if op == 0: + push("#0") + op += 1 + + if n == d: + n = f"#{get_int(0)}" + elif n >= d: + idx = get_int(n-d-1) + n, sym = symbol_ref(idx) + elif op < 3: + n, sym = symbol_ref(n) + + if op > 4: + # This is either a lambda, or the outer proc that wraps the whole program. + body = pop() + if not stack: + n = body + break + else: + params_lbl = asm.next_label("params") + # print(f"{repr(n)}; {body}") + # HACK: somehow values over 3 are already strings + if isinstance(n, str) and n.startswith("#"): + print(f"already hash-prefixed: {n}") + n = n[1:] + emit_rib(params_lbl, f"#{n}", "#0", body) + # FIXME: is this even close? + proc_label = asm.next_label("proc") + asm.label(proc_label) + asm.instr(f"@{params_lbl}") + asm.instr("@rib_nil") + asm.instr("#1") + n = f"@{proc_label}" + op = 4 + + # HACK: this seems to happen with integer constants and slot numbers. + # Make it happen in the right place? + if isinstance(n, int): + n = f"#{n}" + + instr_lbl = emit_instr(op-1, n, pop(), sym) + push(f"@{instr_lbl}") + + sym = None + + # This will be the body of the outer proc, so just "jump" straight to it: + start_instr = n + # Note: emit_instr would want to choose the label... + # emit_rib("main", "#0", start_instr, "#0", comment=f"jump {start_instr}") + # Note: there is no true no-op, and "id" is not yet initialized. This will leave junk on the + # stack. What we really want is to put the "main" label on start_instr when it's emitted. + # Using an illegal opcode here just to ensure we never actually try to interpret it. + emit_rib("main", "#42", "#0", start_instr) + + +BUILTINS = { + **big.BUILTIN_SYMBOLS, + + # Low-memory "registers": + "SP": 0, + "PC": 1, + "NEXT_RIB": 2, # TODO: "LAST_RIB" is more often useful; you write some values, then you save the address somewhere + + "PRIMITIVE_CONT": 3, # where to go when primitive handler is done + + "SYMBOL_TABLE": 4, # Only used during intiialization? + + # General-purpose temporary storage for the interpreter/primitives: + "TEMP_0": 5, + "TEMP_1": 6, + "TEMP_2": 7, + "TEMP_3": 8, + + # Useful values/addresses: + "FIRST_RIB_MINUS_ONE": big.HEAP_BASE-1, + "MAX_RIB": big.HEAP_TOP, + + # The largest value that can ever be the index of a slot, as opposed to the address of a global (symbol) + # TODO: adjust for encoded rib pointers + "MAX_SLOT": big.ROM_BASE-1, +} +"""Constants (mostly addresses) that are used in the generated assembly.""" + + +def tag_int(val): + """Encode an integer value so the runtime will interpret it as a signed integer.""" + return val + +def tag_rib_pointer(addr): + """Encode an address so the runtime will interpret it as a pointer to a rib.""" + assert addr < -big.ROM_BASE or addr >= big.ROM_BASE + return addr + +FIRST_RIB = BUILTINS["FIRST_RIB_MINUS_ONE"] + 1 +assert BUILTINS["MAX_SLOT"] < tag_rib_pointer(FIRST_RIB) + + +def asm_interpreter(): + """ROM program implementing the RVM runtime, which interprets a program stored as "ribs" in ROM and RAM. + + This part of the ROM is the same, independent of the Scheme program that's being interpreted. + """ + + asm = AssemblySource() + + RIB_PROC = "rib_rib" + FALSE = "rib_false" + TRUE = "rib_true" + NIL = "rib_nil" + + def rib_append(val="D"): + """Add a word to the rib currently being constructed. + + Always called three times in succession to construct a full rib. + The value is either "D" (the default), or a value the ALU can produce (0 or 1, basically). + """ + + asm.instr("@NEXT_RIB") + asm.instr("M=M+1") + asm.instr("A=M-1") + asm.instr(f"M={val}") + + def pop(dest): + """Remove the top entry from the stack, placing the value in `dest`. + + Updates only SP and `dest`. + """ + asm.comment("TODO: check SP.z == 0") + asm.comment(f"{dest} = SP.x") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("D=M") + asm.instr(f"@{dest}") + asm.instr("M=D") + asm.comment("SP = SP.y") + asm.instr("@SP") + asm.instr("A=M+1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("M=D") + + def push(val="D"): + """Add a new entry to the stack with the value from D (or 0 or 1, in case that's ever useful.) + + Updates SP and NEXT_RIB. + """ + rib_append(val) + asm.instr("@SP") + asm.instr("D=M") + rib_append() + rib_append(0) # pair + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + asm.instr("@SP") + asm.instr("M=D") + + + asm.label("start") + asm.comment("NEXT_RIB = FIRST_RIB (HEAP_BASE)") + asm.instr("@FIRST_RIB_MINUS_ONE") # Note: this value is one lower than the actual start of the heap, to fit in 15 bits + asm.instr("D=A+1") # Tricky: add 1 to bring the address up to 0x8000 + asm.instr("@NEXT_RIB") + asm.instr("M=D") + asm.blank() + + asm.comment("Construct the symbol table in RAM:") + asm.comment("SYMBOL_TABLE = '()") + asm.instr("@rib_nil") + asm.instr("D=A") + asm.instr("@SYMBOL_TABLE") + asm.instr("M=D") + + asm.comment("R5 = table start") + asm.instr("@symbol_names_start") + asm.instr("D=A") + asm.instr("@TEMP_0") + asm.instr("M=D") + + symbol_table_loop = "symbol_table_loop" + asm.label(symbol_table_loop) + + # asm.comment("DEBUG: log the pointer to tty") + # asm.instr("@TEMP_0") + # asm.instr("D=M") + # asm.instr("@KEYBOARD") + # asm.instr("M=D") + # asm.instr("A=D") + # asm.instr("D=M") + # asm.instr("@KEYBOARD") + # asm.instr("M=D") + + asm.comment("new symbol, with value = MEM(R5+1) and name = MEM(R5)") + asm.instr("@TEMP_0") + asm.instr("A=M+1") + asm.instr("D=M") + rib_append() + asm.instr("@TEMP_0") + asm.instr("A=M") + asm.instr("D=M") + rib_append() + asm.instr("@2") # symbol + asm.instr("D=A") + rib_append() + asm.blank() + + # TODO: these `pair` ribs are actually constant and could live in the ROM, with pre-computed + # addresses pointing to the `symbol` ribs being allocated just above. + # Actually, is this list even used by the library (`string->symbol`)? + asm.comment("SYMBOL_TABLE = new pair") + asm.comment("car = the rib we just wrote") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + rib_append() + asm.comment("cdr = (old) SYMBOL_TABLE") + asm.instr("@SYMBOL_TABLE") + asm.instr("D=M") + rib_append() + rib_append("0") # pair + asm.blank() + + asm.comment("update SYMBOL_TABLE") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + asm.instr("@SYMBOL_TABLE") + asm.instr("M=D") + + # asm.comment("DEBUG: log NEXT_RIB to tty") + # asm.instr("@NEXT_RIB") + # asm.instr("D=M") + # asm.instr("@KEYBOARD") + # asm.instr("M=D") + + asm.comment("increment R5 (by 2)") + asm.instr("@2") + asm.instr("D=A") + asm.instr("@TEMP_0") + asm.instr("M=M+D") + + asm.comment("D = compare(R5, symbol_names_end)") + asm.instr("D=M") + asm.instr("@symbol_names_end") + asm.instr("D=D-A") + asm.instr(f"@{symbol_table_loop}") + asm.instr("D;JLT") + + asm.blank() + + + # + # Initialize interpreter state: + # + + asm.comment("SP = primordial continuation rib") + asm.instr("@rib_outer_cont") + asm.instr("D=A") + asm.instr("@SP") + asm.instr("M=D") + + asm.blank() + + # Note: main is a meaningless instruction with its next the actual entry point, so it's actually + # skipped on the way into the interpreter loop. + asm.comment("PC = @main") + asm.instr("@main") + asm.instr("D=A") + asm.instr("@PC") + asm.instr("M=D") + + asm.blank() + + # + # Exec loop: + # + + def x_to_d(rib_addr_loc): + """Load the first field of a rib to D.""" + asm.instr(f"@{rib_addr_loc}") + asm.instr("A=M") + asm.instr("D=M") + + def y_to_d(rib_addr_loc): + """Load the middle field of a rib to D.""" + asm.instr(f"@{rib_addr_loc}") + asm.instr("A=M+1") + asm.instr("D=M") + + def z_to_d(rib_addr_loc): + """Load the last field of a rib to D.""" + # Note: could save an instruction here if the pointer pointed to the *middle* field. + asm.instr(f"@{rib_addr_loc}") + asm.instr("A=M+1") + asm.instr("A=A+1") # Cheeky: add two ones instead of using @2 to save a cycle + asm.instr("D=M") + + def d_is_not_slot(): + """Test if the value in D is a slot index, leaving 0 in D if so.""" + # FIXME: choose the correct boundary when tagged ints and pointers are implemented + asm.instr("@0x03FF") # Mask for bits that can be set in a number less than 2^10 = 1024 + asm.instr("A=!A") + asm.instr("D=D&A") + + def find_cont(dest): + """Loop over the stack to find the first continuation (a non-pair.) + + No registers are affected except `dest`. + """ + + cont_loop_test = asm.next_label("cont_loop_test") + cont_loop_end = asm.next_label("cont_loop_end") + + asm.comment("R5 = RAM[SP]") + asm.instr("@SP") + asm.instr("D=M") + asm.instr(f"@{dest}") + asm.instr("M=D") + asm.label(cont_loop_test) + z_to_d(dest) + asm.instr(f"@{cont_loop_end}") + asm.instr("D;JNE") + + asm.comment("R5 = R5.y") + y_to_d(dest) + asm.instr(f"@{dest}") + asm.instr("M=D") + asm.instr(f"@{cont_loop_test}") + asm.instr("0;JMP") + + asm.label(cont_loop_end) + + def find_slot(dest): + """Loop over the stack to find the slot referred to by the current instruction, and placing + the location of the object in the supplied destination. + + Overwrites TEMP_0. + """ + assert dest != "TEMP_0" + + test_label = asm.next_label("slot_test") + end_label = asm.next_label("slot_end") + + asm.comment("R5 = idx") + y_to_d("PC") + asm.instr("@TEMP_0") + asm.instr("M=D") + + asm.comment(f"{dest} = SP") + asm.instr("@SP") + asm.instr("D=M") + asm.instr(f"@{dest}") + asm.instr("M=D") + + asm.label(test_label) + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr(f"@{end_label}") + asm.instr("D;JLE") + + asm.comment(f"{dest} = cdr({dest})") + asm.instr(f"@{dest}") + asm.instr("A=M+1") + asm.instr("D=M") + asm.instr(f"@{dest}") + asm.instr("M=D") + # asm.instr("@KEYBOARD"); asm.instr("M=D") # DEBUG + + asm.comment("TEMP_0 -= 1") + asm.instr("@TEMP_0") + asm.instr("M=M-1") + asm.instr(f"@{test_label}") + asm.instr("0;JMP") + + asm.label(end_label) + + + def unimp(): + asm.comment("TODO") + asm.instr("@halt_loop") + asm.instr("0;JMP") + asm.blank() + + + asm.comment("First time: start with the 'next' of main (by falling through to continue_next)") + + asm.comment("Typical loop path: get the next instruction to interpret from the third field of the current instruction:") + asm.label("continue_next") + z_to_d("PC") + asm.instr("@PC") + asm.instr("M=D") + + asm.comment("sanity check: zero is never the address of a valid instr") + asm.instr("@halt_loop") + asm.instr("D;JEQ") + asm.comment("...fallthrough") + asm.blank() + + asm.label("exec_loop") + + # TODO: if CHECK: + asm.comment("Sanity check: instruction type between 0 and 5") + x_to_d("PC") + asm.instr("@halt_loop") + asm.instr("D;JLT") + asm.instr("@5") + asm.instr("D=D-A") + asm.instr("@halt_loop") + asm.instr("D;JGT") + + # Note: this indexed jump doesn't seem to save any cycles vs 5 branches, but it's parallel + # to the primitive handler dispatcher, so maybe easier to follow? + asm.comment("indexed jump to instruction handler") + x_to_d("PC") + asm.instr("@opcode_handler_table") + asm.instr("A=A+D") + asm.instr("A=M") + asm.instr("0;JMP") + asm.blank() + + asm.label("opcode_handler_table") + asm.instr("@opcode_0") + asm.instr("@opcode_1") + asm.instr("@opcode_2") + asm.instr("@opcode_3") + asm.instr("@opcode_4") + asm.instr("@opcode_5") + asm.blank() + + asm.label("opcode_0") + asm.comment("type 0: jump/call") + + asm.comment("TEMP_3 = address of the proc rib") + y_to_d("PC") + d_is_not_slot() + asm.instr("@proc_from_slot") + asm.instr("D;JEQ") + + asm.label("proc_from_global") + asm.comment("TEMP_3 = proc rib from symbol") + y_to_d("PC") + asm.instr("A=D") # HACK: just load it to A from the instr? + asm.instr("D=M") + asm.instr("@TEMP_3") + asm.instr("M=D") + asm.instr("@handle_proc_start") + asm.instr("0;JMP") + asm.blank() + + asm.label("proc_from_slot") + asm.comment("TEMP_3 = proc rib from stack") + find_slot("TEMP_3") + asm.instr("@TEMP_3") # This has the address of the symbol + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@TEMP_3") # Now update to the value (the proc) + asm.instr("M=D") + asm.blank() + + asm.label("handle_proc_start") + asm.instr("D=D") # no-op to make the label traceable + + # TODO: if type_checking: + asm.label("check_proc_rib") + # asm.instr("@TEMP_3"); asm.instr("D=M"); asm.instr("@KEYBOARD"); asm.instr("M=D") # DEBUG + z_to_d("TEMP_3") + asm.instr("D=D-1") + asm.instr("@halt_loop") + asm.instr("D;JNE") + asm.blank() + + asm.comment("Now if next is 0 -> jump; otherwise -> call") + z_to_d("PC") + asm.instr("@handle_call") + asm.instr("D;JNE") + + asm.label("handle_jump") + + asm.comment("Check primitive or closure:") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@0x1F") # Mask off bits that are zero iff it's a primitive + asm.instr("A=!A") + asm.instr("D=D&A") + asm.instr("@handle_jump_to_closure") + asm.instr("D;JNE") + +# when a primitive is called through a jump instruction, ... before the result is pushed to +# the stack the RVM’s stack and pc variables are updated according to the continuation in +# the current stack frame which contains the state of those variables when the call was +# executed (the details are given in Section 2.7). + asm.label("handle_jump_to_primitive") + asm.comment("set target to continue after handling the op") + asm.instr("@after_primitive_for_jump") + asm.instr("D=A") + asm.instr("@PRIMITIVE_CONT") + asm.instr("M=D") + asm.instr("@handle_primitive") + asm.instr("0;JMP") + + asm.label("after_primitive_for_jump") + + asm.comment("find the continuation rib: first rib on stack with non-zero third field") + find_cont("TEMP_0") + + asm.comment("overwrite the top stack entry: SP.y = R5.x") + x_to_d("TEMP_0") + asm.instr("@SP") + asm.instr("A=M+1") + asm.instr("M=D") + + asm.comment("PC = R5.z") + z_to_d("TEMP_0") + asm.instr("@PC") + asm.instr("M=D") + asm.instr("@exec_loop") + asm.instr("0;JMP") + asm.blank() + + + asm.label("handle_jump_to_closure") + + asm.comment("find the continuation rib: first rib on stack with non-zero third field") + find_cont("TEMP_0") + + # New continuation rib. Can't update the existing continuation, in case it's the primordial + # one, which is stored in ROM. + # TODO: move it to RAM, so it can be updated in place? + asm.comment("TEMP_1 = new continuation = (old.x, proc, old.z)") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@TEMP_1") + asm.instr("M=D") + + asm.comment("New cont. saved stack = old.x") + asm.instr("@TEMP_0") + asm.instr("A=M") + asm.instr("D=M") + rib_append() + asm.comment("New cont. proc = TEMP_3") + asm.instr("@TEMP_3") + asm.instr("D=M") + rib_append() + asm.comment("New cont. next instr = old.z") + asm.instr("@TEMP_0") + asm.instr("A=M+1") + asm.instr("A=A+1") + asm.instr("D=M") + rib_append() + + def wrangle_closure_params(): + """Move num_args objects from the current stack to a new stack on top of a new continuation rib. + + The continuation rib is not modified (but the reference to it in TEMP_1 is overwritten.) + + Before: + TEMP_3 = addr of proc rib + TEMP_1 = addr of new continuation (just allocated) + + During: + TEMP_2 = loop var: num args remaining + TEMP_1 = loop var: top of new stack + TEMP_0 = overwritten + + After: + TEMP_1 = new top of stack + """ + + asm.comment("TEMP_2 = num_args (proc.x.x)") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@TEMP_2") + asm.instr("M=D") + # asm.instr("@KEYBOARD"); asm.instr("M=D") # DEBUG + + params_test = asm.next_label("params_test") + params_end = asm.next_label("params_end") + + asm.label(params_test) + asm.instr("@TEMP_2") + asm.instr("D=M") + asm.instr(f"@{params_end}") + asm.instr("D;JLE") + + # TODO: modify the stack entry in place? And fix up SP, then + asm.comment("pop one object and add it to the new stack") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + rib_append() + asm.instr("@TEMP_1") + asm.instr("D=M") + rib_append() + rib_append(0) + + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + asm.instr("@TEMP_1") + asm.instr("M=D") + + asm.instr("@TEMP_2") + asm.instr("M=M-1") + asm.instr(f"@{params_test}") + asm.instr("0;JMP") + asm.label(params_end) + + wrangle_closure_params() + + asm.comment("Put new stack in place: SP = TEMP_1") + asm.instr("@TEMP_1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("M=D") + + asm.comment("PC = proc.x.z") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("A=A+1") + asm.instr("D=M") + asm.instr("@PC") + asm.instr("M=D") + + asm.instr("@exec_loop") + asm.instr("0;JMP") + asm.blank() + + # "next" is not 0, so this is a call + asm.label("handle_call") + + asm.comment("Check primitive or closure:") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@0x1F") # Mask off bits that are zero iff it's a primitive + asm.instr("A=!A") + asm.instr("D=D&A") + asm.instr("@handle_call_closure") + asm.instr("D;JNE") + + asm.label("handle_call_primitive") + asm.comment("set target to continue after handling the op") + asm.instr("@continue_next") + asm.instr("D=A") + asm.instr("@PRIMITIVE_CONT") + asm.instr("M=D") + asm.instr("@handle_primitive") + asm.instr("0;JMP") + + asm.label("handle_call_closure") + + asm.comment("R6 = new rib for the continuation") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@TEMP_1") + asm.instr("M=D") + rib_append(0) # cont.x gets filled in later + asm.instr("@TEMP_3") + asm.instr("D=M") + rib_append() # cont.y = TEMP_3 (the proc rib) + z_to_d("PC") + rib_append() # cont.z = pc.z (next instr after the call) + asm.blank() + + wrangle_closure_params() + + asm.comment("TEMP_2 = SP (old stack to save)") + asm.instr("@SP") + asm.instr("D=M") + asm.instr("@TEMP_2") + asm.instr("M=D") + asm.comment("SP = TEMP_1 (top of new stack)") + asm.instr("@TEMP_1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("M=D") + asm.comment("cont.x = TEMP_2 (saved stack)") + find_cont("TEMP_1") # Hmm. Searching here just because we ran out of TEMPs to hold onto it + asm.instr("@TEMP_2") + asm.instr("D=M") + asm.instr("@TEMP_1") + asm.instr("A=M") + asm.instr("M=D") + + asm.comment("PC = proc.x.z") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("A=A+1") + asm.instr("D=M") + asm.instr("@PC") + asm.instr("M=D") + + asm.instr("@exec_loop") + asm.instr("0;JMP") + asm.blank() + + + asm.label("opcode_1") + asm.comment("type 1: set") + y_to_d("PC") + d_is_not_slot() + asm.instr("@handle_set_slot") + asm.instr("D;JEQ") + + asm.label("handle_set_global") + asm.comment("R5 = address of symbol rib") + y_to_d("PC") + asm.instr("@TEMP_0") + asm.instr("M=D") + asm.comment("RAM[TEMP_0] = pop()") + pop("TEMP_1") + asm.instr("@TEMP_1") + asm.instr("D=M") + asm.instr("@TEMP_0") + asm.instr("A=M") + asm.instr("M=D") + asm.instr("@continue_next") + asm.instr("0;JMP") + asm.blank() + + asm.label("handle_set_slot") + unimp() + + asm.label("opcode_2") + asm.comment("type 2: get") + y_to_d("PC") + d_is_not_slot() + asm.instr("@handle_get_slot") + asm.instr("D;JEQ") + + asm.label("handle_get_global") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@TEMP_0") + asm.instr("M=D") + y_to_d("PC") # D = address of symbol + asm.instr("A=D") # TODO: load directly to A? + asm.instr("D=M") # D = object at symbol.x + rib_append("D") + asm.instr("@SP") + asm.instr("D=M") + rib_append("D") + rib_append("0") # pair + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("M=D") + + asm.instr("@continue_next") + asm.instr("0;JMP") + asm.blank() + + asm.label("handle_get_slot") + find_slot("TEMP_3") + asm.instr("@TEMP_3") # This has the address of the stack entry + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@TEMP_3") # Now update to the value + asm.instr("M=D") + asm.blank() + + # TODO: macro for push from TEMP + asm.comment("push TEMP3") + asm.instr("@TEMP_3") + asm.instr("D=M") + rib_append() + asm.instr("@SP") + asm.instr("D=M") + rib_append() + rib_append(0) + asm.comment("SP = NEXT_RIB-3 (the one we just initialized)") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + asm.instr("@SP") + asm.instr("M=D") + + asm.instr("@continue_next") + asm.instr("0;JMP") + asm.blank() + + asm.label("opcode_3") + asm.comment("type 3: const") + y_to_d("PC") + asm.comment("TODO: check tag") + asm.comment("Allocate rib: x = pc.y") + rib_append() + asm.comment("y = SP") + asm.instr("@SP") + asm.instr("D=M") + rib_append() + asm.comment("z = 0 (pair)") + rib_append("0") + asm.blank() + + asm.comment("SP = just-allocated rib") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + asm.instr("@SP") + asm.instr("M=D") + asm.blank() + + asm.instr("@continue_next") + asm.instr("0;JMP") + asm.blank() + + asm.label("opcode_4") + asm.comment("type 4: if") + pop("TEMP_0") + asm.comment("TOS is #f; no branch") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@rib_false") + asm.instr("D=D-A") + asm.instr("@continue_next") + asm.instr("D;JEQ") + asm.comment("PC = PC.y") + y_to_d("PC") + asm.instr("@PC") + asm.instr("M=D") + asm.instr("@exec_loop") + asm.instr("0;JMP") + asm.blank() + + # Note: a safety check would have the same result, but this makes it explicit in case we ever + # make those checks optional. + asm.label("opcode_5") + asm.comment("type 5: halt") + asm.instr("@halt_loop") + asm.instr("0;JMP") + asm.blank() + + # + # Halt: + # + + halt_label = "halt_loop" + asm.label(halt_label) + asm.instr(f"@{halt_label}") + asm.instr("0;JMP") + + + # + # Primitive handling: + # + + asm.label("handle_primitive") + asm.instr("@TEMP_3") + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@primitive_vector_table_start") + asm.instr("A=A+D") + asm.instr("A=M") + asm.instr("0;JMP") + asm.blank() + + asm.comment("=== Primitive vectors ===") + asm.label("primitive_vector_table_start") + asm.comment("0") + asm.instr("@primitive_rib") + asm.comment("1") + asm.instr("@primitive_id") + asm.comment("2") + asm.instr("@primitive_arg1") + asm.comment("3") + asm.instr("@primitive_arg2") + asm.comment("4") + asm.instr("@primitive_close") + asm.comment("5") + asm.instr("@primitive_rib?") + asm.comment("6") + asm.instr("@primitive_field0") + asm.comment("7") + asm.instr("@primitive_field1") + asm.comment("8") + asm.instr("@primitive_field2") + asm.comment("9") + asm.instr("@primitive_field0-set!") + asm.comment("10") + asm.instr("@primitive_field1-set!") + asm.comment("11") + asm.instr("@primitive_field2-set!") + asm.comment("12") + asm.instr("@primitive_eqv?") + asm.comment("13") + asm.instr("@primitive_<") + asm.comment("14") + asm.instr("@primitive_+") + asm.comment("15") + asm.instr("@primitive_-") + asm.comment("16") + asm.instr("@primitive_*") + asm.comment("17 (quotient: not implemented)") + asm.instr("@primitive_unimp") + asm.comment("18") + asm.instr("@primitive_getchar") + asm.comment("19 (putchar: not implemented)") + asm.instr("@primitive_unimp") + asm.comment("20") + asm.instr("@primitive_peek") + asm.comment("21") + asm.instr("@primitive_poke") + asm.comment("22") + asm.instr("@primitive_halt") + # fatal? + asm.comment("dummy handlers for 23-31 to simplify range check above:") + for op in range(23, 32): + asm.comment(f"{op} (dummy)") + asm.instr("@primitive_unimp") + asm.label("primitive_vectors_end") + + asm.blank() + + def return_from_primitive(): + asm.instr("@PRIMITIVE_CONT") + asm.instr("A=M") + asm.instr("0;JMP") + asm.blank() + + asm.label("primitive_rib") + asm.comment("primitive 0; rib :: x y z -- rib(x, y, z)") + pop("TEMP_2") + pop("TEMP_1") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + rib_append() + asm.instr("@TEMP_1") + asm.instr("D=M") + rib_append() + asm.instr("@TEMP_2") + asm.instr("D=M") + rib_append() + + # TODO: pop only two, then modify the top of stack in place to save one allocation + asm.comment("push allocated rib") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@3") + asm.instr("D=D-A") + push("D") + return_from_primitive() + + + asm.label("primitive_id") + asm.comment("primitive 1; id :: x -- x") + asm.comment("... and, that's all folks") + return_from_primitive() + + + asm.label("primitive_arg1") + asm.comment("primitive 2; arg1 :: x y -- x") # i.e. "drop" + asm.comment("Simply discard the top entry on the stack by updating SP") + asm.instr("@SP") + asm.instr("A=M+1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_arg2") + asm.comment("primitive 3; arg2 :: x y -- y") + asm.comment("Discard the second entry on the stack by updating the top entry") + asm.comment("D = the addr of the third entry from the top of the stack") + asm.instr("@SP") + asm.instr("A=M+1") + asm.instr("A=M+1") + asm.instr("D=M") + asm.comment("SP.x = D") + asm.instr("@SP") + asm.instr("A=M+1") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_close") + asm.comment("primitive 4; close :: x -- rib(x[0], stack, 1)") + asm.comment("TEMP_0 = new closure/proc rib") + asm.instr("@NEXT_RIB") + asm.instr("D=M") + asm.instr("@TEMP_0") + asm.instr("M=D") + asm.comment("TEMP_0.x = TOS.x") + x_to_d("SP") + asm.instr("A=D") + asm.instr("D=M") + rib_append() + asm.comment("TEMP_0.y = SP.y") + y_to_d("SP") + rib_append() + asm.comment("TEMP_0.z = 1 (proc type)") + rib_append("1") + asm.comment("Modify top stack entry in place") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_rib?") + asm.comment("primitive 5; rib? :: x -- bool(x is a rib)") + # FIXME: this test is super bogus. Need to implement tagged ints, and test the correct bit here. + # For now, any value large enough to be a potential address + + is_rib_nonneg_label = asm.next_label("is_rib_nonneg") + is_rib_true_label = asm.next_label("is_rib_true") + + asm.instr("@SP") + asm.instr("A=M") + asm.instr("D=M") + asm.instr(f"@{is_rib_nonneg_label}") + asm.instr("D;JGE") + asm.instr("D=-D") + asm.label(is_rib_nonneg_label) + asm.comment("If the (absolute) value is larger than the base address of the ROM, assume it's a rib address") + asm.instr("@ROM") + asm.instr("D=D-A") + asm.instr(f"@{is_rib_true_label}") + asm.instr("D;JGT") + + asm.instr("@rib_false") + asm.instr("D=A") + # FIXME: jump to shared copy of this common sequence + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + asm.label(is_rib_true_label) + asm.instr("@rib_true") + asm.instr("D=A") + # FIXME: jump to shared copy of this common sequence + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field0") + asm.comment("primitive 6; field0 :: rib(x, _, _) -- x") + asm.comment("Update in place: SP.x.x = SP.x.x.x") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field1") + asm.comment("primitive 7; field1 :: rib(_, y, _) -- y") + asm.comment("Update in place: SP.x.x = SP.x.x.y") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field2") + asm.comment("primitive 8; field2 :: rib(_, _, z) -- z") + asm.comment("Update in place: SP.x.x = SP.x.x.z") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("A=A+1") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field0-set!") + asm.comment("primitive 9; field0-set! :: rib(_, y, z) x -- x (and update the rib in place: rib(x, y, z))") + asm.comment("TEMP_0 = pop() = x") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("Update the rib in place: SP.x.x = x") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M") + asm.instr("M=D") + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field1-set!") + asm.comment("primitive 10; field1-set! :: rib(x, _, z) y -- y (and update the rib in place: rib(x, y, z))") + asm.comment("TEMP_0 = pop() = y") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("Update the rib in place: SP.x.y = y") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("M=D") + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_field2-set!") + asm.comment("primitive 11; field2-set! :: rib(x, y, _) z -- z (and update the rib in place: rib(x, y, z))") + asm.comment("TEMP_0 = pop() = x") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("Update the rib in place: SP.x.x = x") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("A=M+1") + asm.instr("A=A+1") + asm.instr("M=D") + # FIXME: jump to shared copy of this common sequence + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_eqv?") + asm.comment("primitive 12; eqv? :: x y -- bool(x is identical to y)") + asm.comment("TEMP_0 = pop() = y") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("D=D-M") + eqv_true_label = asm.next_label("eqv_true") + asm.instr(f"@{eqv_true_label}") + asm.instr("D;JEQ") + asm.instr("@rib_false") + asm.instr("D=A") + # FIXME: jump to shared copy of this common sequence + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + asm.label(eqv_true_label) + asm.instr("@rib_true") + asm.instr("D=A") + # FIXME: jump to shared copy of this common sequence + asm.comment("Update the top stack entry in place") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_<") + asm.comment("primitive 13; < :: x y -- bool(x < y)") + # TODO: if type_checking: + asm.comment("TEMP_0 = pop() = y") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("TEMP_0 -= (SP.x = x)") + asm.instr("@SP") + asm.instr("A=M") # A = addr of SP.x + asm.instr("D=D-M") # D = y - x + # Note: range of ints is limited, so true overflow doesn't happen? + is_less_label = asm.next_label("is_less") + asm.instr(f"@{is_less_label}") + asm.instr("D;JGT") + asm.instr("@rib_false") + asm.instr("D=A") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + asm.label(is_less_label) + asm.instr("@rib_true") + asm.instr("D=A") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + asm.label("primitive_+") + asm.comment("primitive 14; + :: x y -- x + y") + # TODO: if type_checking: + asm.comment("D = pop() = y") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("SP.x += y") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=M+D") + return_from_primitive() + + asm.label("primitive_-") + asm.comment("primitive 15; - :: x y -- x - y") + # TODO: if type_checking: + asm.comment("D = pop() = y") + pop("TEMP_0") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.comment("SP.x -= y") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=M-D") + return_from_primitive() + + + asm.label("primitive_*") + asm.comment("primitive 16; * :: x y -- x * y") + asm.comment("TEMP_0 = pop() = y") + pop("TEMP_0") + asm.comment("TEMP_1 = SP.x = x") + x_to_d("SP") + asm.instr("@TEMP_1") + asm.instr("M=D") + + asm.comment("TEMP_2 = bit mask = 1") + asm.instr("@TEMP_2") + asm.instr("M=1") + + asm.comment("TEMP_3 = acc = 0") + asm.instr("@TEMP_3") + asm.instr("M=0") + + mul_test_label = asm.next_label("mul_test") + mul_end_label = asm.next_label("mul_end") + mul_next_label = asm.next_label("mul_next") + + asm.label(mul_test_label) + asm.comment("if y == 0, exit") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr(f"@{mul_end_label}") + asm.instr("D;JEQ") + + asm.comment("if y & mask != 0, acc += (shifted) x") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@TEMP_2") + asm.instr("D=D&M") + asm.instr(f"@{mul_next_label}") + asm.instr("D;JEQ") + + asm.instr("@TEMP_1") + asm.instr("D=M") + asm.instr("@TEMP_3") + asm.instr("M=D+M") + + asm.label(mul_next_label) + asm.comment("y &= ~mask") + asm.instr("@TEMP_2") + asm.instr("D=!M") + asm.instr("@TEMP_0") + asm.instr("M=M&D") + asm.comment("mask <<= 1") + asm.instr("@TEMP_2") + asm.instr("D=M") + asm.instr("M=M+D") + asm.comment("x <<= 1") + asm.instr("@TEMP_1") + asm.instr("D=M") + asm.instr("M=M+D") + + asm.instr(f"@{mul_test_label}") + asm.instr("0;JMP") + + asm.label(mul_end_label) + asm.comment("SP.x = acc") + asm.instr("@TEMP_3") + asm.instr("D=M") + asm.instr("@SP") + asm.instr("A=M") + asm.instr("M=D") + return_from_primitive() + + + asm.label("primitive_getchar") + asm.comment("primitive 18: getchar :: -- (blocks until a key is pressed)") + # Note: this will only catch keypresses that occur after the instruction is executed. For + # a responsive shell, the check will have to incorporated into the interpreter loop. + # It might even need to be be checked more often than once per instruction, since instructions + # can take as long as hundreds of cycles. + asm.comment("Loop until @KEYBOARD contains anything other than zero.") + + getchar_loop_label = asm.next_label("getchar_loop") + + asm.label(getchar_loop_label) + asm.instr("@KEYBOARD") + asm.instr("D=M") + asm.instr(f"@{getchar_loop_label}") + asm.instr("D;JEQ") + + push("D") + return_from_primitive() + + + asm.label("primitive_peek") + asm.comment("primitive 19; peek :: x -- RAM[x]") + unimp() + + + asm.label("primitive_poke") + asm.comment("primitive 20; poke :: x y -- y (and write the value y at RAM[x])") + asm.comment("R5 = value") + pop("TEMP_0") + asm.comment("R6 = addr") + pop("TEMP_1") + asm.instr("@TEMP_0") + asm.instr("D=M") + asm.instr("@TEMP_1") + asm.instr("A=M") + asm.instr("M=D") + push("D") + return_from_primitive() + + + asm.label("primitive_halt") + asm.comment("primitive 21; halt :: -- (no more instructions are executed)") + asm.instr("@halt_loop") + asm.instr("0;JMP") + asm.blank() + + + asm.label("primitive_unimp") + asm.comment("Note: the current instr will be logged if tracing is enabled") + asm.instr("@halt_loop") + asm.instr("0;JMP") + asm.blank() + + + asm.label("interpreter_end") + asm.blank() + + return asm + + +def jack_interpreter(): + from nand.solutions import solved_10 + from alt import reg + + asm = AssemblySource() + + def init_global(comment, addr, value): + asm.comment(comment) + if isinstance(value, int) and -1 <= value <= 1: + asm.instr(f"@{addr}") + asm.instr(f"M={value}") + else: + asm.instr(f"@{value}") + asm.instr("D=A") + asm.instr(f"@{addr}") + asm.instr("M=D") + + # Because the base of the ROM isn't at address 0, we make it explicit for the assembler: + asm.label("start") + + init_global("Jack stack pointer", "SP", 256) + # Note: these two probably don't actually need to be initialized, but might contain garbage + # and confuse debugging + init_global("Jack frame pointer", "LCL", 0) + init_global("Jack arg pointer", "ARG", 0) + # THIS and THAT definitely don't need to be set up before the first function call + asm.blank() + + translator = reg.Translator(asm) + + def load_class(path): + with open(path) as f: + src_lines = f.readlines() + ast = solved_10.parse_class("".join(src_lines)) + + ir = reg.compile_class(ast) + + translator.translate_class(ir) + + for cl in "Interpreter", "Rib": + load_class(f"alt/scheme/{cl}.jack") + + asm.label("interpreter_end") + asm.blank() + + return asm + + +def first_loop_in_function(symbols, class_name, function_name): + """Address of the first instruction labeled "loop_... found (probably) within the given function.""" + + function_label = f"{class_name}.{function_name}".lower() + symbols_by_addr = sorted((addr, name) for name, addr in symbols.items()) + ptr = 0 + while symbols_by_addr[ptr][1] != function_label: + ptr += 1 + ptr += 1 + while not symbols_by_addr[ptr][1].startswith("loop_"): + ptr += 1 + return symbols_by_addr[ptr][0] + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Run Scheme source with text-mode display and keyboard") + parser.add_argument("path", nargs=argparse.ONE_OR_MORE, help="Path to source (.scm)") + parser.add_argument("--simulator", action="store", default="codegen", help="One of 'vector' (slower, more precise); 'codegen' (faster, default); 'compiled' (experimental)") + parser.add_argument("--trace", action="store_true", help="Print each Ribbit instruction as it is interpreted. Note: runs almost 3x slower.") + parser.add_argument("--print", action="store_true", help="Print interpreter assembly and compiled instructions.") + # TEMP: experimental for now + parser.add_argument("--asm", action="store_true", help="Use the (partially-implemented) assembly interpreter.") + + args = parser.parse_args() + + src_lines = [] + for p in args.path: + with open(p) as f: + src_lines += [] + f.readlines() + + run("".join(src_lines), + interpreter="jack" if not args.asm else "assembly", + simulator=args.simulator, + print_asm=args.print, + trace_level=TRACE_COARSE if args.trace else TRACE_NONE) + # trace_level=TRACE_FINE if args.trace else TRACE_NONE) + + +if __name__ == "__main__": + main() diff --git a/alt/scheme/test_rvm.py b/alt/scheme/test_rvm.py new file mode 100755 index 0000000..6b531ad --- /dev/null +++ b/alt/scheme/test_rvm.py @@ -0,0 +1,447 @@ +#! /usr/bin/env pytest + +import pytest + +from alt import big +from alt.scheme import rvm +from alt.scheme.inspector import Inspector +import nand +from nand.translate import AssemblySource +from nand.vector import unsigned + + +SHOW_ALL_LABELS = False + + +def parameterize(f): + def vector(program, **args): + return run_to_halt(program, simulator="vector", interpreter="jack", **args) + def codegen(program, **args): + return run_to_halt(program, simulator="codegen", interpreter="jack", **args) + def assembly(program, **args): + return run_to_halt(program, simulator="codegen", interpreter="assembly", **args) + return pytest.mark.parametrize("run", [vector, codegen, assembly])(f) + + +def run_jack(program): + return run_to_halt(program=program, simulator="codegen", interpreter="jack") + + +@parameterize +def test_trivial(run): + program = "42" + + inspect, output = run(program) + + assert inspect.stack() == [42] + assert output == [] + + +@parameterize +def test_string(run): + program = '"abc"' + + inspect, output = run(program) + + assert inspect.stack() == ["abc"] + assert output == [] + + +@parameterize +def test_lt(run): + program = several("(< 1 2)", "(< 1 1)", "(< 2 1)") + + inspect, output = run(program) + + assert inspect.stack() == [[True, False, False]] + assert output == [] + + +@parameterize +def test_add(run): + program = "(+ 1 2)" + + inspect, output = run(program) + + assert inspect.stack() == [3] + assert output == [] + + +@parameterize +def test_sub(run): + program = "(- 123 234)" + + inspect, output = run(program) + + assert inspect.stack() == [-111] + assert output == [] + + +@parameterize +def test_mul(run): + program = "(* 6 7)" + + inspect, output = run(program) + + assert inspect.stack() == [42] + assert output == [] + + +@parameterize +def test_mul_mixed_signs(run): + program = several("(* 6 -7)", "(* -14 -3)", "(* 2 -21)") + + inspect, output = run(program) + + assert inspect.stack() == [[-42, 42, -42]] + assert output == [] + + +# Note: not implemented in assembly +def test_quotient(): + program = "(quotient 123 10)" + + inspect, output = run_jack(program) + + assert inspect.stack() == [12] + assert output == [] + + +# Note: not implemented in assembly +def test_quotient_mixed_signs(): + program = several("(quotient 6 -3)", "(quotient -14 -3)", "(quotient 21 -2)") + + inspect, output = run_jack(program) + + assert inspect.stack() == [[-2, 4, -10]] + assert output == [] + + +@parameterize +def test_if(run): + program = "(if #t 42 '())" + + inspect, output = run(program) + + assert inspect.stack() == [42] + assert output == [] + + +@parameterize +def test_quote(run): + program = "'(1 2 3)" + + inspect, output = run(program) + + assert inspect.stack() == [[1, 2, 3]] + assert output == [] + + +@parameterize +def test_lambda(run): + program = """ + ((lambda (x y) (+ x y)) + 14 28) + """ + + inspect, output = run(program) + + assert inspect.stack() == [42] + assert output == [] + + +@parameterize +def test_define(run): + program = """ + (define (cons x y) (rib x y 0)) + + (cons 1 '(2)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [[1, 2]] + assert output == [] + + +@parameterize +def test_fact(run): + program = """ + (define (fact n) + (if (< n 2) 1 + (* n (fact (- n 1))))) + + (fact 5) + """ + + inspect, output = run(program) + + assert inspect.stack() == [120] + assert output == [] + + +@parameterize +def test_capture(run): + program = """ + (define (add x) (lambda (y) (+ x y))) + ((add 1) 2) + """ + + inspect, output = run(program) + + assert inspect.stack() == [3] + assert output == [] + + +@parameterize +def test_draw(run): + program = """ + (define poke (rib 21 0 1)) + (define screen 1024) + (poke screen 65) + """ + + inspect, output = run(program) + + assert inspect.stack() == [65] + assert output == [] + + +@parameterize +def test_tty(run): + program = """ + (define poke (rib 21 0 1)) + (define keyboard 4095) + (poke keyboard 48) + """ + + inspect, output = run(program) + + assert inspect.stack() == [48] + assert output == [ord('0')] + + +@parameterize +def test_eval(run): + with open("alt/scheme/ribbit/min.scm") as f: + lib = f.readlines() + + program = "\n".join(lib + ["""(eval '(+ 1 2))"""]) + + inspect, output = run(program) + + assert inspect.stack() == [3] + assert output == [] + + +# +# Tests for specific primitives: +# + +@parameterize +def test_field0(run): + program = """ + (define field0 (rib 6 0 1)) + + (field0 '(7 8 9)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [7] # car + assert output == [] + + +@parameterize +def test_field1(run): + program = """ + (define field1 (rib 7 0 1)) + + (field1 '(7 8 9)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [[8, 9]] # cdr + assert output == [] + +@parameterize +def test_field2(run): + program = """ + (define field2 (rib 8 0 1)) + + (field2 '(7 8 9)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [0] # pair-type + assert output == [] + + +@parameterize +def test_field0_set(run): + program = """ + (define field0-set! (rib 9 0 1)) + (define (pair x y) (rib x y 0)) + + (define foo '(7 8 9)) + + ;; Leave both the modified-in-place cons cell and the value on the stack for inspection: + (pair foo (field0-set! foo 10)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [([10, 8, 9], 10, 0)] + assert output == [] + + +@parameterize +def test_field1_set(run): + program = """ + (define field1-set! (rib 10 0 1)) + (define (pair x y) (rib x y 0)) + + (define foo '(7 8 9)) + + ;; Leave both the modified-in-place cons cell and the value on the stack for inspection: + (pair foo (field1-set! foo '(10 11))) + """ + + inspect, output = run(program) + + # Note: here the cdr is acually a list, so it gets interpreted that way + assert inspect.stack() == [[[7, 10, 11], 10, 11]] + assert output == [] + + +@parameterize +def test_field2_set(run): + program = """ + (define field2-set! (rib 11 0 1)) + (define (pair x y) (rib x y 0)) + + (define foo '(7 8 9)) + + ;; Hard to make a legit value by changing the type, so just update it and then pull it back out + (pair (field2-set! foo 11) (field2 foo)) + """ + + inspect, output = run(program) + + assert inspect.stack() == [(11, 11, 0)] + assert output == [] + + +@parameterize +def test_eqv_simple(run): + program = """ + (define eqv? (rib 12 0 1)) + + (define x 42) + (define y 42) ;; same value means same object + (define z 11) + + """ + several("(eqv? x x)", "(eqv? x y)", "(eqv? x z)") + + inspect, output = run(program) + + assert inspect.stack() == [[True, True, False]] + assert output == [] + + +@parameterize +def test_eqv_ribs(run): + program = """ + (define eqv? (rib 12 0 1)) + + (define x '(1 2 3)) + (define y '(1 2 3)) ;; not the same object, despite same contents + (define z '(3 4 5)) + + """ + several("(eqv? x x)", "(eqv? x y)", "(eqv? x z)") + + inspect, output = run(program) + + assert inspect.stack() == [[True, False, False]] + assert output == [] + + +@parameterize +def test_ribq(run): + program = """ + (define rib? (rib 5 0 1)) + + """ + several("(rib? 123)", "(rib? -345)", "(rib? #t)", '(rib? "abc")') + + inspect, output = run(program) + + assert inspect.stack() == [[False, False, True, True]] + assert output == [] + + +# +# Helpers +# + +def several(*exprs): + """Make a program that evaluates several expressions and constructs a list with each result.""" + def go(xs): + # print(xs) + if xs == []: + return " '()" + else: + return f"(cons {xs[0]}\n{go(xs[1:])})" + return "(define (cons $$x $$y) (rib $$x $$y 0))\n" + go(list(exprs)) + + +def run_to_halt(program, interpreter, max_cycles=250000, simulator="codegen"): + """Compile and run a Scheme program, then return a function for inspecting the RAM, and a + list of words that were written to the TTY port. + """ + + encoded = rvm.compile(program) + # print(f"encoded program: {repr(encoded)}") + + instrs, symbols, stack_loc, pc_loc, next_rib_loc, interp_loop_addr, halt_loop_addr = rvm.assemble(encoded, interpreter, True) + + computer = nand.syntax.run(big.BigComputer, simulator=simulator) + + computer.init_rom(instrs) + + # Jump over low memory that we might be using for debugging: + computer.poke(0, big.ROM_BASE) + computer.poke(1, big.parse_op("0;JMP")) + + cycles = 0 + output = [] + + inspect = Inspector(computer, symbols, stack_loc) + + while not (computer.fetch and computer.pc == halt_loop_addr): + if cycles > max_cycles: + raise Exception(f"exceeded max_cycles: {max_cycles:,d}") + + computer.ticktock() + cycles += 1 + + tty_char = computer.get_tty() + if tty_char: + print(f"tty: {tty_char}") + output.append(tty_char) + + if computer.fetch and inspect.is_labeled(computer.pc): + if SHOW_ALL_LABELS: + print(f"{inspect.show_addr(computer.pc)}") + if computer.pc == interp_loop_addr: + print(f"{cycles:,d}") + stack = ", ".join(str(x) for x in inspect.stack()) + print(f" stack: {stack}") + pc = inspect.peek(pc_loc) + print(f" pc: {inspect.show_addr(pc)}") + print(f" {inspect.show_instr(pc)}") + + print(f"cycles: {cycles}") + + return (inspect, output) diff --git a/alt/sp.py b/alt/sp.py index 07fcbd8..11da4fd 100755 --- a/alt/sp.py +++ b/alt/sp.py @@ -252,11 +252,17 @@ def _pop_segment(self, segment_name, segment_ptr, index): self.asm.instr("M=D") def _push_d(self): - # TODO: no need for this as soon as everything's switched to use SP++ directly + """Push the value in D. When the value came from memory, it still needs to be stored in D + temporarily so the push op can access the bus. So the superclass's implementation will + use this one-cycle push sequence, and there's no need to re-implement those operarations + here.""" self.asm.instr("SP++=D") def _pop_d(self): - # TODO: no need for this as soon as everything's switched to use --SP directly? + """Pop the top of the statck to D. When the value will be written to memory, it still + needs to be stored in D temporarily so the pop op can access the bus. So the superclass's + implementation will use this one-cycle pop sequence, and there's no need to re-implement + those operarations here.""" self.asm.instr("D=--SP") def _binary(self, opcode, op): @@ -386,6 +392,8 @@ def _call(self): # TODO: improve the common sequence for `return`. + # That would save one cycle per word of saved state per call, but minimal ROM space, since + # the sequence is shared. def finish(self): diff --git a/alt/test_big.py b/alt/test_big.py new file mode 100755 index 0000000..4572b5c --- /dev/null +++ b/alt/test_big.py @@ -0,0 +1,695 @@ +#! /usr/bin/env pytest + +import pytest + +from nand import run, gate_count +import nand.component +from alt.big import * + + +# TODO: put this somewhere common: +def parameterize_simulators(f): + def vector(chip, **args): + return nand.run(chip, simulator="vector", **args) + def codegen(chip, **args): + return nand.run(chip, simulator="codegen", **args) + return pytest.mark.parametrize("run", [vector, codegen])(f) + +@parameterize_simulators +def test_memory_system(run): + mem = run(FlatMemory) + + # set RAM[0] = -1 + mem.in_ = -1 + mem.load = 1 + mem.address = 0 + mem.tick(); mem.tock() + + # RAM[0] holds value + mem.in_ = 9999 + mem.load = 0 + mem.tick(); mem.tock() + assert mem.out == -1 + + # Did not also write to upper RAM or Screen (same lower bits) + mem.address = SCREEN_BASE + assert mem.out == 0 + mem.address = HEAP_BASE + assert mem.out == 0 + + # Set RAM[0x0200] = 2222 + mem.in_ = 2222 + mem.load = 1 + mem.address = 0x0200 + mem.tick(); mem.tock() + assert mem.out == 2222 + + # RAM[0x0200] holds value + mem.in_ = 9999 + mem.load = 0 + mem.tick(); mem.tock() + assert mem.out == 2222 + + # Did not also write to lower RAM or Screen + mem.address = 0 + assert mem.out == -1 + mem.address = SCREEN_BASE + assert mem.out == 0 + + # Low order address bits connected + # (note: not actually testing anything in this system?) + mem.address = 0x0001; assert mem.out == 0 + mem.address = 0x0002; assert mem.out == 0 + mem.address = 0x0004; assert mem.out == 0 + mem.address = 0x0008; assert mem.out == 0 + mem.address = 0x0010; assert mem.out == 0 + mem.address = 0x0020; assert mem.out == 0 + mem.address = 0x0040; assert mem.out == 0 + mem.address = 0x0080; assert mem.out == 0 + mem.address = 0x0100; assert mem.out == 0 + mem.address = 0x0200; assert mem.out == 2222 + mem.address = 0x0400; assert mem.out == 0 + # mem.address = 0x0800; assert mem.out == 0 + # mem.address = 0x1000; assert mem.out == 0 + # mem.address = 0x2000; assert mem.out == 0 + + # RAM[0x0123] = 1234 + mem.address = 0x0123 + mem.in_ = 1234 + mem.load = 1 + mem.tick(); mem.tock() + assert mem.out == 1234 + + # Did not also write to upper RAM or Screen + mem.address = SCREEN_BASE + 0x0123 + assert mem.out == 0 + mem.address = HEAP_BASE + 0x0123 + assert mem.out == 0 + + # RAM[0x0234] = 2345 + mem.address = 0x0234 + mem.in_ = 2345 + mem.load = 1 + mem.tick(); mem.tock() + assert mem.out == 2345 + + # Did not also write to lower RAM or Screen + mem.address = 0x0034 + assert mem.out == 0 + mem.address = 0x0434 + assert mem.out == 0 + + + ### Keyboard test + + ## Note: this test can't be done on the isolated MemorySystem, because the necessary + ## connections are only provided when the simulator detects the full Computer is being + ## simulated. Instead, we test it below. + # mem.address = 0x6000 + # assert mem.out == 75 + + + ### Screen test + + mem.load = 1 + mem.in_ = -1 + mem.address = SCREEN_BASE + 100 + mem.tick(); mem.tock() + assert mem.out == -1 + + # ...load still set + mem.address = SCREEN_BASE + 977 + mem.tick(); mem.tock() + assert mem.out == -1 + + +@parameterize_simulators +def test_rom(run): + mem = run(FlatMemory) + + # Writes to any ROM-mapped address are ignored: + for addr in (ROM_BASE, ROM_BASE + 1234, ROM_BASE + 27*1024, HEAP_BASE - 1): + mem.address = addr + mem.in_ = -1 + mem.load = 1 + mem.ticktock() + + print(addr, mem.out) + assert mem.out == 0 + + + # Initialize with successive integers in the first ROM locations: + mem.init_rom([0]*ROM_BASE + list(range(100))) + + for i in range(100): + mem.address = ROM_BASE + i + assert mem.out == i + + # A write has no effect; you still read the ROM value + mem.address = ROM_BASE + 15 + mem.in_ = 123 + mem.load = 1 + mem.ticktock() + assert mem.out == 15 + + +def test_gates_mem(): + """Portion of the extra chip size that's in the memory system. + + This would have been extra chips on the board, not extra gates in the CPU, presumably. + """ + + assert gate_count(FlatMemory)['nands'] == 219 + + import project_05 + assert gate_count(project_05.MemorySystem)['nands'] == 163 + + +@parameterize_simulators +def test_decode_alu(run): + decode = run(DecodeALU) + + # A typical computation with memory access: + decode.instruction = parse_op("M=M-D") + + assert decode.is_alu_instr + + assert decode.mem_to_alu + + assert not decode.zx + assert not decode.nx + assert not decode.zy + assert decode.ny + assert decode.f + assert decode.no + + assert not decode.alu_to_a + assert not decode.alu_to_d + assert decode.alu_to_m + + + # A typical conditional branch: + decode.instruction = parse_op("D;JLE") + + assert decode.is_alu_instr + + # assert decode.mem_to_alu # unused + + assert not decode.zx + assert not decode.nx + assert decode.zy + assert decode.ny + assert not decode.f + assert not decode.no + + assert not decode.alu_to_a + assert not decode.alu_to_d + assert not decode.alu_to_m + + + decode.instruction = parse_op("@1234") + + assert not decode.is_alu_instr + + assert not decode.alu_to_a + assert not decode.alu_to_d + assert not decode.alu_to_m + + +def test_gates_decode_alu(): + assert gate_count(DecodeALU)['nands'] == 6 + + +@parameterize_simulators +def test_decode_jump(run): + decode = run(DecodeJump) + + + # A typical computation with memory access: + decode.instruction = parse_op("M=M-D") + + assert not decode.jump + + + # A typical conditional branch: + decode.instruction = parse_op("D;JLE") + decode.ng = 1 + decode.zr = 0 + + assert decode.jump + + +def test_gates_decode_jump(): + assert gate_count(DecodeJump)['nands'] == 18 + + +# def test_cpu(chip=BigCPU): +# cpu = nand.run(chip) + +# cpu.instruction = 0b0011000000111001 # @12345 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 12345 and cpu.pc == 1 # and DRegister == 0 + +# cpu.instruction = 0b1110110000010000 # D=A +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 12345 and cpu.pc == 2 # and DRegister == 12345 + +# cpu.instruction = 0b0101101110100000 # @23456 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 23456 and cpu.pc == 3 # and DRegister == 12345 + +# cpu.instruction = 0b1110000111010000 # D=A-D +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 23456 and cpu.pc == 4 # and DRegister == 11111 + +# cpu.instruction = 0b0000001111101000 # @1000 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 5 # and DRegister == 11111 + +# cpu.instruction = 0b1110001100001000 # M=D +# cpu.ticktock() +# assert cpu.outM == 11111 and cpu.writeM == 1 and cpu.addressM == 1000 and cpu.pc == 6 # and DRegister == 11111 + +# cpu.instruction = 0b0000001111101001 # @1001 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1001 and cpu.pc == 7 # and DRegister == 11111 + +# # Note confusing timing here: outM has the value to be written to memory when the clock falls. Afterward, +# # outM has a nonsense value. +# # TODO: always assert outM and writeM before tick/tock? +# cpu.instruction = 0b1110001110011000 # MD=D-1 +# assert cpu.outM == 11110 and cpu.writeM == 1 and cpu.addressM == 1001 and cpu.pc == 7 # and DRegister == 11111 +# cpu.ticktock() +# assert cpu.outM == 11109 and cpu.writeM == 1 and cpu.addressM == 1001 and cpu.pc == 8 # and DRegister == 11110 + +# cpu.instruction = 0b0000001111101000 # @1000 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 9 # and DRegister == 11110 + +# cpu.instruction = 0b1111010011010000 # D=D-M +# cpu.inM = 11111 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 10 # and DRegister == -1 + +# cpu.instruction = 0b0000000000001110 # @14 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 14 and cpu.pc == 11 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000100 # D;jlt +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 14 and cpu.pc == 14 # and DRegister == -1 + +# cpu.instruction = 0b0000001111100111 # @999 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 999 and cpu.pc == 15 # and DRegister == -1 + +# cpu.instruction = 0b1110110111100000 # A=A+1 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 16 # and DRegister == -1 + +# cpu.instruction = 0b1110001100001000 # M=D +# cpu.ticktock() +# assert cpu.outM == -1 and cpu.writeM == 1 and cpu.addressM == 1000 and cpu.pc == 17 # and DRegister == -1 + +# cpu.instruction = 0b0000000000010101 # @21 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 21 and cpu.pc == 18 # and DRegister == -1 + +# cpu.instruction = 0b1110011111000010 # D+1;jeq +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 21 and cpu.pc == 21 # and DRegister == -1 + +# cpu.instruction = 0b0000000000000010 # @2 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 2 and cpu.pc == 22 # and DRegister == -1 + +# cpu.instruction = 0b1110000010010000 # D=D+A +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 2 and cpu.pc == 23 # and DRegister == 1 + +# cpu.instruction = 0b0000001111101000 # @1000 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 24 # and DRegister == -1 + +# cpu.instruction = 0b1110111010010000 # D=-1 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 25 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000001 # D;JGT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 26 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000010 # D;JEQ +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 27 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000011 # D;JGE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 28 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000100 # D;JLT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000101 # D;JNE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000110 # D;JLE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == -1 + +# cpu.instruction = 0b1110001100000111 # D;JMP +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == -1 + +# cpu.instruction = 0b1110101010010000 # D=0 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000001 # D;JGT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1002 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000010 # D;JEQ +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000011 # D;JGE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000100 # D;JLT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000101 # D;JNE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1002 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000110 # D;JLE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 0 + +# cpu.instruction = 0b1110001100000111 # D;JMP +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 0 + +# cpu.instruction = 0b1110111111010000 # D=1 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000001 # D;JGT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000010 # D;JEQ +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000011 # D;JGE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000100 # D;JLT +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000101 # D;JNE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000110 # D;JLE +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1001 # and DRegister == 1 + +# cpu.instruction = 0b1110001100000111 # D;JMP +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 1000 # and DRegister == 1 + +# cpu.reset = 1 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 1000 and cpu.pc == 0 # and DRegister == 1 + +# cpu.instruction = 0b0111111111111111 # @32767 +# cpu.reset = 0 +# cpu.ticktock() +# assert cpu.writeM == 0 and cpu.addressM == 32767 and cpu.pc == 1 # and DRegister == 1 + + +def test_gates_cpu(): + """Portion of the extra chip size that's in the CPU's "idle" state.""" + + # Note: factoring out instruction decoding seems to have added 2 gates + assert gate_count(IdlableCPU)['nands'] == 1108 + + import project_05 + assert gate_count(project_05.CPU)['nands'] == 1099 + + +@parameterize_simulators +def test_computer_no_program(run): + computer = run(BigComputer) + + for _ in range(100): + computer.ticktock() + + # Two-cycles per instruction: + assert computer.pc == 50 + + +# # Add.hack: +# ADD_PROGRAM = [ +# 0b0000000000000010, # @2 +# 0b1110110000010000, # D=A +# 0b0000000000000011, # @3 +# 0b1110000010010000, # D=D+A +# 0b0000000000000001, # @1 Note: modified to avoid address 0 (SP), which may get special treatment +# 0b1110001100001000, # M=D +# ] + +# def test_computer_add(chip=project_05.Computer, simulator="vector"): +# computer = run(chip, simulator=simulator) + +# # First run (at the beginning PC=0) +# computer.run_program(ADD_PROGRAM) + +# assert computer.peek(1) == 5 + + +# # Reset the PC +# computer.reset = 1 +# computer.ticktock() +# assert computer.pc == 0 + +# # Second run, to check that the PC was reset correctly. +# computer.poke(1, 12345) +# computer.reset = 0 +# while computer.pc < len(ADD_PROGRAM): +# computer.ticktock() + +# assert computer.peek(1) == 5 + + +# MAX_PROGRAM = [ +# # Note: modified to avoid address 0 (SP), which may get special treatment +# 0b0000000000000001, # 0: @1 +# 0b1111110000010000, # 1: D=M +# 0b0000000000000010, # 2: @2 +# 0b1111010011010000, # 3: D=D-M ; D = mem[1] - mem[2] +# 0b0000000000001010, # 4: @10 +# 0b1110001100000001, # 5: D; JGT +# 0b0000000000000010, # 6: @2 +# 0b1111110000010000, # 7: D=M ; D = mem[2] +# 0b0000000000001100, # 8: @12 +# 0b1110101010000111, # 9: JMP +# 0b0000000000000001, # 10: @1 +# 0b1111110000010000, # 11: D=M ; D = mem[1] +# 0b0000000000000011, # 12: @3 +# 0b1110001100001000, # 13: M=D ; mem[3] = max +# 0b0000000000001110, # 14: @14 +# 0b1110101010000111, # 15: JMP ; infinite loop +# ] + +# def test_computer_max(chip=project_05.Computer, simulator="vector", cycles_per_instr=1): +# computer = run(chip, simulator=simulator) + +# computer.init_rom(MAX_PROGRAM) + +# # first run: compute max(3,5) +# computer.poke(1, 3) +# computer.poke(2, 5) +# for _ in range(14*cycles_per_instr): +# computer.ticktock() +# assert computer.peek(3) == 5 + +# # second run: compute max(23456,12345) +# computer.reset_program() +# computer.poke(1, 23456) +# computer.poke(2, 12345) +# # The run on these inputs needs less cycles (different branching) +# for _ in range(10*cycles_per_instr): +# computer.ticktock() +# assert computer.peek(3) == 23456 + +# # Copy one keycode value from the address where the keyboard is mapped to the RAM. +# COPY_INPUT_PROGRAM = [ +# 24576, # @(0x6000) +# 64528, # D=M (D = keycode) +# 1, # @1 +# 58120, # M=D (mem[1] = D) +# 4, # @4 +# 60039, # 0;JMP (infinite loop) +# ] + +# def test_computer_keyboard(chip=project_05.Computer, simulator="vector", cycles_per_instr=1): +# """A value which is presented via a special `Input` component can be read from the +# address 0x6000, where the "keyboard" is mapped. + +# Note: can't test this at the level of MemorySystem, because the wrapper for the full +# computer provides some of the necessary plumbing. +# """ + +# computer = run(chip, simulator=simulator) + +# computer.init_rom(COPY_INPUT_PROGRAM) + +# KEY_A = ord("a") + +# computer.set_keydown(KEY_A) +# for _ in range(4*cycles_per_instr): +# computer.ticktock() + +# assert computer.peek(1) == KEY_A + + +# def test_computer_tty_no_program(chip=project_05.Computer, simulator="vector"): +# """When nothing has been written address 0x6000, no value is available on the TTY "port". +# """ + +# computer = run(chip, simulator=simulator) + +# for _ in range(100): +# computer.ticktock() + +# assert computer.pc == 100 +# assert computer.tty_ready == True +# assert computer.get_tty() == 0 + + +# # Write a few constant values to the external "tty" interface: +# WRITE_TTY_PROGRAM = [ +# 1, # @1 +# 60432, # D=A +# 24576, # @(0x6000) +# 58120, # M=D (write 1) + +# 0, # @0 +# 60432, # D=A +# 24576, # @(0x6000) +# 58120, # M=D ("write" 0; no effect) + +# 12345, # @12345 +# 60432, # D=A +# 24576, # @(0x6000) +# 58120, # M=D (write 12345) + +# 12, # @12 +# 60039, # 0; JMP (infinite loop) +# ] + +# def test_computer_tty(chip=project_05.Computer, simulator="vector", cycles_per_instr=1): +# """A value which is written to the address 0x6000 can be read from outside via +# a special `Output` component. + +# Also, the presence of a value in that component is signalled by the `tty_ready` + +# Note: can't test this at the level of MemorySystem, because the wrapper for the full +# computer provides some of the necessary plumbing. +# """ + +# computer = run(chip, simulator=simulator) + +# computer.init_rom(WRITE_TTY_PROGRAM) + +# # Run until a value appears (after 4 instructions): + +# cycles = 0 +# while computer.tty_ready and cycles < 1000: +# computer.ticktock() +# cycles += 1 + +# print(f"cycles: {cycles}") +# assert computer.tty_ready == False +# assert computer.get_tty() == 1 +# assert computer.tty_ready == True +# assert cycles == 4*cycles_per_instr # Bogus? + + +# # Now run four more instructions; nothing written this time: + +# for _ in range(4*cycles_per_instr): +# computer.ticktock() + +# assert computer.tty_ready == True +# assert computer.get_tty() == 0 +# assert computer.tty_ready == True + + +# # One more time, with a different value: + +# cycles = 0 +# while computer.tty_ready and cycles < 1000: +# computer.ticktock() +# cycles += 1 + +# assert computer.tty_ready == False +# assert computer.get_tty() == 12345 +# assert computer.tty_ready == True +# assert cycles == 4*cycles_per_instr # Bogus? + + + +# def cycles_per_second(chip, cycles_per_instr=1): +# """Estimate the speed of CPU simulation by running Max repeatedly with random input. +# """ + +# import random +# import timeit + +# computer = run(chip) + +# computer.init_rom(MAX_PROGRAM) + +# CYCLES = 14*cycles_per_instr + +# def once(): +# x = random.randint(0, 0x7FFF) +# y = random.randint(0, 0x7FFF) +# computer.reset_program() +# computer.poke(1, x) +# computer.poke(2, y) +# for _ in range(CYCLES): +# computer.ticktock() +# assert computer.peek(3) == max(x, y) + +# count, time = timeit.Timer(once).autorange() + +# return count*CYCLES/time + + +# def test_speed(chip=project_05.Computer, cycles_per_instr=1): +# cps = cycles_per_second(chip, cycles_per_instr) +# print(f"Measured speed: {cps:0,.1f} cycles/s") +# assert cps > 500 # Note: about 1k/s is expected, but include a wide margin for random slowness + + +def test_gates_computer(): + """Overall extra chip size.""" + + # Note: factoring out instruction decoding seems to have added 2 gates + assert gate_count(BigComputer)['nands'] == 1447 + + import project_05 + assert gate_count(project_05.Computer)['nands'] == 1262 + diff --git a/alt/test_reg.py b/alt/test_reg.py index 93abc0e..0dadbbe 100755 --- a/alt/test_reg.py +++ b/alt/test_reg.py @@ -10,6 +10,48 @@ from alt.reg import _Stmt_str +def test_simplify_expression(): + binary = jack_ast.BinaryExpression + def tilde(exp): return jack_ast.UnaryExpression(jack_ast.Op("~"), exp) + x = jack_ast.VarRef("x") + op = jack_ast.Op + const = jack_ast.IntegerConstant + def neg(exp): return jack_ast.UnaryExpression(jack_ast.Op("-"), exp) + + assert (simplify_expression(neg(const(1))) + == const(-1)) + + assert (simplify_expression(binary(x, op("+"), neg(const(1)))) + == binary(x, op("+"), const(-1))) + + assert (simplify_expression(binary(x, op("<"), const(0))) + == binary(x, op("<"), const(0))) + + assert (simplify_expression(tilde(binary(x, op("<"), const(0)))) + == binary(x, op(">="), const(0))) + + assert (simplify_expression(binary(x, op(">"), neg(const(100)))) + == binary(x, op(">"), const(-100))) + + assert (simplify_expression(binary(const(0), op("<"), x)) + == binary(x, op(">"), const(0))) + + # These case might seem fiddly, but they do tend to come up and comparing with 0 + # is just a lot simpler: + assert (simplify_expression(binary(x, op(">"), const(-1))) + == binary(x, op(">="), const(0))) + + assert (simplify_expression(binary(x, op("<="), const(-1))) + == binary(x, op("<"), const(0))) + + assert (simplify_expression(binary(x, op("<"), const(1))) + == binary(x, op("<="), const(0))) + + assert (simplify_expression(binary(x, op(">="), const(1))) + == binary(x, op(">"), const(0))) + + + def test_if_liveness(): src = """ class Main { diff --git a/computer.py b/computer.py index 925c579..8a2346e 100755 --- a/computer.py +++ b/computer.py @@ -4,7 +4,7 @@ The program to run must be in the form of Hack assembly (.asm) or VM opcodes (a directory of .vm files) and is specified by sys.argv[1]. -The `codegen` simulator is used unless --vector is used. +The `codegen` simulator is used unless --simulator is specified. $ ./computer.py examples/Blink.asm @@ -45,6 +45,7 @@ parser.add_argument("--max-fps", action="store", type=int, help="Experimental! (VM/Jack-only) pin the game loop to a fixed rate, approximately (in games that use Sys.wait).\nMay or may not work, depending on the translator.") # TODO: "--max-cps"; limit the clock speed directly. That will allow different chips to be compared (in a way). # TODO: "--headless" with no UI, with Keyboard and TTY connected to stdin/stdout +parser.add_argument("--scale", action="store_true", help="Scale the display by a whole number multiplier to approximately fill the screen.") def main(platform=USER_PLATFORM): args = parser.parse_args() @@ -62,7 +63,8 @@ def main(platform=USER_PLATFORM): src_map=src_map if args.trace else None, is_in_wait=in_function_pred(None if args.no_waiting else wait_addresses), max_fps=args.max_fps, - is_in_halt=in_function_pred(halt_addresses)) + is_in_halt=in_function_pred(halt_addresses), + scale=args.scale) def load(platform, path, print_asm=False, no_waiting=False): @@ -154,18 +156,29 @@ def load(platform, path, print_asm=False, no_waiting=False): ] + [ (c, c) for c in range(32, 127) ]) # Printable characters, plus a few odd-balls +SHIFTED_KEY_MAP = { + **KEY_MAP, + **dict((ord(x), ord(y)) for x, y in + zip("abcdefghijklmnopqrstuvwxyz`1234567890-=[]\\;',./", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+{}|:\"<>?")) +} +"""Map from raw key code to the code produced when (any) shift modifier is down. +Note: this is definitely not correct if your keyboard layout isn't a typical US layout. +Not sure +""" class KVM: - def __init__(self, title, width, height): + def __init__(self, title, width, height, scale=False): self.width = width self.height = height pygame.init() flags = 0 - # flags = pygame.FULLSCREEN - # pygame.SCALED requires 2.0.0 - flags |= pygame.SCALED + # flags |= pygame.FULLSCREEN + if scale: + # pygame.SCALED requires 2.0.0 + flags |= pygame.SCALED self.screen = pygame.display.set_mode((width, height), flags=flags) pygame.display.set_caption(title) @@ -189,7 +202,9 @@ def process_events(self): return typed_keys[0] keys = pygame.key.get_pressed() - for idx, key in KEY_MAP.items(): + mods = pygame.key.get_mods() + shifted = mods & pygame.KMOD_SHIFT or mods & pygame.KMOD_LSHIFT or mods & pygame.KMOD_RSHIFT + for idx, key in (KEY_MAP if not shifted else SHIFTED_KEY_MAP).items(): if keys[idx]: return key @@ -212,11 +227,11 @@ def update_display(self, get_pixel): pygame.display.flip() -def run(program, chip, name="Nand!", simulator="codegen", src_map=None, is_in_wait=(lambda _: False), max_fps=None, is_in_halt=(lambda _: False)): +def run(program, chip, name="Nand!", simulator="codegen", src_map=None, is_in_wait=(lambda _: False), max_fps=None, is_in_halt=(lambda _: False), scale=False): computer = nand.syntax.run(chip, simulator=simulator) computer.init_rom(program) - kvm = KVM(name, 512, 256) + kvm = KVM(name, 512, 256, scale=scale) last_cycle_time = last_event_time = last_display_time = last_frame_time = now = time.monotonic() was_in_sys_wait = False diff --git a/nand/codegen.py b/nand/codegen.py index a7c701f..13274e7 100644 --- a/nand/codegen.py +++ b/nand/codegen.py @@ -13,13 +13,15 @@ - ROM: there should no more than one ROM present - MemorySystem: there should be no more than one MemorySystem present +If the conventional MemorySystem is not used, then these components can be used discretely: +- RAM, Input, Output: not more than one of each + A few more are implemented so that this simulator can also be used (and tested) with smaller chips: - DFF - DMux - DMux8Way - Mux8Way16 -- TODO: RAM, separately from MemorySystem Any other ICs that appear are flattened to combinations of these. The downside is that a moderate amount of flattening will have a significant impact on simulation speed. For example, @@ -36,12 +38,16 @@ the memory layout also entails constructing a new UI harness, which is beside the point. """ -from nand.component import Nand, Const, DFF, ROM +from nand.component import Nand, Const, DFF, ROM, RAM, Input, Output from nand.integration import IC, Connection, root, clock from nand.optimize import simplify from nand.vector import extend_sign +# For debugging: +PRINT_FLATTENED = False +PRINT_GENERATED = False + def run(ic): """Prepare an IC for simulation, returning an object which exposes the inputs and outputs as attributes. If the IC is Computer, it also provides access to the ROM, RAM, etc. @@ -55,7 +61,8 @@ def translate(ic): class_name, lines = generate_python(ic) # print(ic) - # print_lines(lines) + if PRINT_GENERATED: + print_lines(lines) eval(compile('\n'.join(lines), filename="", @@ -124,17 +131,21 @@ def generate_python(ic, inline=True, prefix_super=False, cython=False): ic = ic.flatten(primitives=PRIMITIVES) # ic = simplify(ic.flatten(primitives=PRIMITIVES)) # TODO: don't flatten everything in simplify - # print(ic) + if PRINT_FLATTENED: + print(ic) all_comps = ic.sorted_components() if any(conn == clock for conn in ic.wires.values()): raise NotImplementedError("This simulator cannot handle chips that refer to 'clock' directly.") - if any(isinstance(c, IC) and c.label == 'MemorySystem' for c in all_comps): + # if any(isinstance(c, IC) and c.label == 'MemorySystem' for c in all_comps): + if any(isinstance(c, ROM) for c in all_comps): supr = "SOC" + supr_args = ["15", "16"] # HACK: works for Big; doesn't break the standard CPU else: supr = "Chip" + supr_args = [] lines = [] def l(indent, str): @@ -237,6 +248,10 @@ def binary16(comp, template): return template.format(a=src_many(comp, 'a'), b=src_many(comp, 'b')) def component_expr(comp): + """Expression producing the value for a component's single output, or None if it can't be + represented that way. + """ + if isinstance(comp, Nand): return binary1(comp, "not ({a} and {b})") elif isinstance(comp, Const): @@ -300,6 +315,14 @@ def component_expr(comp): # the RAM? address = src_many(comp, 'address', 14) return f"self._ram[{address}] if 0 <= {address} < 0x4000 else (self._screen[{address} & 0x1fff] if 0x4000 <= {address} < 0x6000 else (self._keyboard if {address} == 0x6000 else 0))" + elif isinstance(comp, RAM): + address = src_many(comp, 'address', comp.address_bits) + return f"self._ram[{address}]" + elif isinstance(comp, Input): + return "self._keyboard" + elif isinstance(comp, Output): + # FIXME: bogus? + return "self._tty == 0" else: raise Exception(f"Unrecognized primitive: {comp}") @@ -310,7 +333,7 @@ def component_expr(comp): l(0, f"class {class_name}({supr}):") l(1, f"def __init__(self):") - l(2, f"{supr}.__init__(self)") + l(2, f"{supr}.__init__({','.join(['self'] + supr_args)})") for name in ic.inputs(): l(2, f"self._{name} = 0 # input") for name in ic.outputs(): @@ -345,7 +368,12 @@ def component_expr(comp): pass elif isinstance(comp, ROM): # TODO: trap index errors with try/except - l(3, f"{output_name(comp)} = self._rom[{src_many(comp, 'address', comp.address_bits)}]") + address_name = f"_{all_comps.index(comp)}_address" + l(3, f"{address_name} = {src_many(comp, 'address', comp.address_bits)}") + l(3, f"if 0 <= {address_name} < len(self._rom):") + l(4, f"{output_name(comp)} = self._rom[{address_name}]") + l(3, "else:") + l(4, f"{output_name(comp)} = 0") elif comp.label == "DMux": in_name = f"_{all_comps.index(comp)}_in" sel_name = f"_{all_comps.index(comp)}_sel" @@ -437,8 +465,22 @@ def component_expr(comp): l(6, f"self._tty = {in_name}") l(6, f"self._tty_ready = {in_name} != 0") any_state = True - elif isinstance(comp, (Const, ROM)): + elif isinstance(comp, (Const, ROM, Input)): pass + elif isinstance(comp, RAM): + address_expr = src_many(comp, 'address', 14) + in_name = f"_{all_comps.index(comp)}_in" + l(4, f"if {src_one(comp, 'load')}:") + l(5, f"{in_name} = {src_many(comp, 'in_')}") + l(5, f"self._ram[{address_expr}] = {in_name}") + any_state = True + elif isinstance(comp, Output): + in_name = f"_{all_comps.index(comp)}_in" + l(4, f"if {src_one(comp, 'load')}:") + l(5, f"{in_name} = {src_many(comp, 'in_')}") + l(5, f"self._tty = {in_name}") + l(5, f"self._tty_ready = {in_name} != 0") + any_state = True elif comp.label in PRIMITIVES: # All combinational components: nothing to do here pass @@ -499,10 +541,11 @@ def ticktock(self, cycles=1): class SOC(Chip): """Super for chips that include a full computer with ROM, RAM, keyboard input, and "TTY" output.""" - def __init__(self): - self._rom = [] - self._ram = [0]*(1 << 14) - self._screen = [0]*(1 << 13) + def __init__(self, rom_address_bits=15, ram_address_bits=14, screen_address_bits=13): + self._rom = [0]*(1 << rom_address_bits) + self._ram = [0]*(1 << ram_address_bits) + if screen_address_bits is not None: + self._screen = [0]*(1 << screen_address_bits) self._keyboard = 0 self._tty = 0 @@ -519,14 +562,14 @@ def init_rom(self, instructions): size = len(instructions) # Assuming a 15-bit ROM, as we do here, we can't address more than 32K of instructions: - if size >= 2**15: - raise Exception(f"Too many instructions: {size:0,d} >= {2**15:0,d}") + if size >= len(self._rom): + raise Exception(f"Too many instructions: {size:0,d} >= {len(self._rom):,d}") contents = instructions + [ size, # @size (which is the address of this instruction) 0b111_0_000000_000_111, # JMP ] - self._rom = contents + self._rom[:len(contents)] = contents # TODO: surprisingly, this is not faster (no apparent effect): # self._rom = array.array('H', contents) @@ -564,6 +607,9 @@ def poke_screen(self, address, value): """Write a value to the display RAM. Address must be between 0x000 and 0x1FFF.""" self._screen[address] = extend_sign(value) + def peek_rom(self, address): + return self._rom[address] + def set_keydown(self, keycode): """Provide the code which identifies a single key which is currently pressed.""" self._keyboard = keycode diff --git a/nand/jack_ast.py b/nand/jack_ast.py index ff78687..d63f614 100644 --- a/nand/jack_ast.py +++ b/nand/jack_ast.py @@ -33,7 +33,11 @@ class ArrayRef(NamedTuple): array_index: "ExpressionRec" class SubroutineCall(NamedTuple): - """Note: either class_name or var_name or neither may be present.""" + """Note: either class_name or var_name or neither may be present. + Also: this is the only kind of expression that can have side effects. For example, the callee + might update a static or field or write to memory. Therefore order of evaluation needs to + be preserved only when these are involved. + """ class_name: Optional[str] var_name: Optional[str] diff --git a/nand/solutions/solved_06.py b/nand/solutions/solved_06.py index fa68834..f7d6e55 100644 --- a/nand/solutions/solved_06.py +++ b/nand/solutions/solved_06.py @@ -45,6 +45,11 @@ "MP": 0b111, } +def register_names(count): + """Names for the first locations in low memory: R0, R1, ...""" + return { f"R{i}": i for i in range(count) } + + BUILTIN_SYMBOLS = { **{ "SP": 0, @@ -55,7 +60,7 @@ "SCREEN": 0x4000, "KEYBOARD": 0x6000, }, - **{ f"R{i}": i for i in range(16)} + **register_names(16) } @@ -68,11 +73,17 @@ def parse_op(string, symbols=None): :param symbols: a dictionary mapping symbol names to addresses (of labels in the code and memory locations allocated for "static" variables.) Note: not used in this implementation, but included in the signature in so that other compatible parsers can use it. + + Extra syntax: + - Constant values may be specified in hex, e.g. @0x5555 """ - m = re.match(r"@((?:0x)?\d+)", string) + m = re.match(r"@((0x[0-9a-fA-F]+)|([1-9][0-9]*)|0)", string) if m: - return eval(m.group(1)) + value = eval(m.group(1)) + if value < 0 or value > 0x7FFF: + raise ParseError(f"A-command value out of range: {value}") + return value else: m = re.match(r"(?:([ADM]+)=)?([^;]+)(?:;J(..))?", string) if m: @@ -90,7 +101,7 @@ def parse_op(string, symbols=None): alu = ALU_CONTROL[alu_str] m_for_a = int('M' in m.group(2)) else: - raise Exception(f"unrecognized alu op: {m.group(2)}") + raise ParseError(f"unrecognized alu op: {m.group(2)}") jmp_str = m.group(3) if jmp_str is None: @@ -98,14 +109,14 @@ def parse_op(string, symbols=None): elif jmp_str in JMP_CONTROL: jmp = JMP_CONTROL[jmp_str] else: - raise Exception(f"unrecognized jump: J{m.group(3)}") + raise ParseError(f"unrecognized jump: J{m.group(3)}") return (0b111 << 13) | (m_for_a << 12) | (alu << 6) | (dest << 3) | jmp else: - raise Exception(f"unrecognized: {string}") + raise ParseError(f"unrecognized: {string}") -def assemble(lines, parse_op=parse_op, min_static=16, max_static=255): +def assemble(lines, parse_op=parse_op, min_static=16, max_static=255, start_addr=0, builtins=BUILTIN_SYMBOLS): """Parse a sequence of lines them as assembly commands, accounting for builtin symbols, labels, and variables. @@ -132,13 +143,13 @@ def assemble(lines, parse_op=parse_op, min_static=16, max_static=255): # Second pass: resolve labels to locations symbols = {} - loc = 0 + loc = start_addr for line in code_lines: m = re.match(r"\((.*)\)", line) if m: name = m.group(1) - if name in BUILTIN_SYMBOLS: - raise Exception(f"Attempt to redefine builtin symbol {name} at location {loc}") + if name in builtins: + raise ParseError(f"Attempt to redefine builtin symbol {name} at location {loc}") elif name in symbols: # This isn't an error because allowing re-definition makes it easy to hackishly # override something (see alt/shift.py). Sorry, world. @@ -148,7 +159,7 @@ def assemble(lines, parse_op=parse_op, min_static=16, max_static=255): loc += 1 # Third pass: parse all other instructions, and resolve non-label symbols (i.e. "static" allocations.) - ops = [] + ops = [0]*start_addr statics = {} next_static = min_static for line in code_lines: @@ -158,19 +169,27 @@ def assemble(lines, parse_op=parse_op, min_static=16, max_static=255): m = re.match(r"@(\D.*)", line) if m: name = m.group(1) - if name in BUILTIN_SYMBOLS: - ops.append(BUILTIN_SYMBOLS[name]) + if name in builtins: + ops.append(builtins[name]) elif name in symbols: ops.append(symbols[name]) elif name in statics: ops.append(statics[name]) else: - if next_static > max_static: - raise Exception(f"Unable to allocate static storage for symbol {name}; already used all {max_static - min_static + 1} available locations") - statics[name] = next_static - ops.append(next_static) - next_static += 1 + if next_static is None: + raise ParseError(f"Unable to allocate static storage for symbol {name}; no static allocation space available") + elif next_static > max_static: + raise ParseError(f"Unable to allocate static storage for symbol {name}; already used all {max_static - min_static + 1} available locations") + else: + statics[name] = next_static + ops.append(next_static) + next_static += 1 else: ops.append(parse_op(line, symbols)) return (ops, symbols, statics) + + +class ParseError(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) diff --git a/nand/solutions/solved_12/Math.jack b/nand/solutions/solved_12/Math.jack index e45e414..622636c 100644 --- a/nand/solutions/solved_12/Math.jack +++ b/nand/solutions/solved_12/Math.jack @@ -69,7 +69,7 @@ class Math { } /** Returns the integer part of x/y. - * When a Jack compiler detects the multiplication operator '/' in the + * When a Jack compiler detects the division operator '/' in the * program's code, it handles it by invoking this method. In other words, * the Jack expressions x/y and divide(x,y) return the same value. */ diff --git a/nand/translate.py b/nand/translate.py index 1734f5c..d0f78ad 100644 --- a/nand/translate.py +++ b/nand/translate.py @@ -108,6 +108,20 @@ def find_function(self, class_name, function_name): return start, ends + def pretty(self, start_addr=0): + """Lines of code, including the location in ROM of each instruction, as a geneerator.""" + loc = start_addr + for l in self.lines: + raw = l.strip() + if raw == "" or raw.startswith("//"): + yield f" {l}" + elif raw.startswith("("): + yield f" {l}" + else: + yield f"{loc:5d}: {l}" + loc += 1 + + # TODO: find a better home for this (nand.runtime? .execute?, .debug?) def run(self, assembler, computer, stop_cycles=None, debug=False, tty=None): """Step through the execution of the generated program, using the provided assembler and diff --git a/nand/vector.py b/nand/vector.py index 6828a17..e67c227 100644 --- a/nand/vector.py +++ b/nand/vector.py @@ -27,11 +27,7 @@ def run(ic, optimize = True): if optimize: ic = simplify(ic) nv, stateful = synthesize(ic) - if any(isinstance(c, ROM) for c in ic.sorted_components()): - w = NandVectorComputerWrapper(nv, stateful) - else: - w = NandVectorWrapper(nv) - return w + return NandVectorWrapper(nv, stateful) def synthesize(ic): @@ -519,11 +515,40 @@ def custom_op(f): class NandVectorWrapper: - """Convenient syntax around a NandVector. You get one of these from run(chip). + """Wrapper with extra operations for the full Computer, when the expected components are found. + + If there are any ROMs, init_rom operates on one, chosen arbitrarily. + + If there are any RAMs, peek/poke operate on the largest RAM. + If there is more than one RAM, peek_/poke_screen operate on the *second* largest RAM. + + If there is a ROM and an output "pc", run_program is can be used to run a sequence of + instructions completely. + + If there is an input "reset", reset_program will use it to reset the processor state. + + If there is any Input, set_keydown will set the keycode that can be read through it. + + If there is any Output, get_tty will read the last character written to it, and reset it's + "ready" flag. """ - def __init__(self, vector): + def __init__(self, vector, stateful): self._vector = vector + self._stateful = stateful + + def nth(seq, n): + lst = list(seq) + if len(lst) > n: + return lst[n] + + self._rom = nth((c for c in self._stateful if isinstance(c, ROMOps)), 0) + rams_by_size_desc = sorted((c for c in self._stateful if isinstance(c, RAMOps)), + key=lambda c: c.comp.address_bits, reverse=True) + self._mem = nth(rams_by_size_desc, 0) + self._screen = nth(rams_by_size_desc, 1) + self._keyboard = nth((c for c in self._stateful if isinstance(c, InputOps)), 0) + self._tty = nth((c for c in self._stateful if isinstance(c, OutputOps)), 0) def __setattr__(self, name, value): """Set the value of a single- or multiple-bit input.""" @@ -583,33 +608,15 @@ def outputs(self): def internal(self): return dict([(name, self.get_internal(name)) for (name, _) in self._vector.internal.keys()]) - def components(self, types): - """List of internal components (e.g. RAM, ROM). - - Note: types should be one or more of the types defined in nand.component, not the wrappers - with the same names defined in this module. - """ - return [c for c in self._components if isinstance(c, types)] - - def __repr__(self): - return str(self.outputs()) - -class NandVectorComputerWrapper(NandVectorWrapper): - """Wrapper with extra operations for the full Computer.""" - - def __init__(self, vector, stateful): - NandVectorWrapper.__init__(self, vector) - self._stateful = stateful - self._rom, = [c for c in self._stateful if isinstance(c, ROMOps)] - self._mem, = [c for c in self._stateful if isinstance(c, RAMOps) and c.comp.address_bits == 14] - self._screen, = [c for c in self._stateful if isinstance(c, RAMOps) and c.comp.address_bits == 13] - self._keyboard, = [c for c in self._stateful if isinstance(c, InputOps)] - self._tty, = [c for c in self._stateful if isinstance(c, OutputOps)] + # High-level def run_program(self, instructions): """Install and run a sequence of instructions, stopping when pc runs off the end.""" + if ('pc', 0) not in self._vector.outputs: + raise MissingComponent("'pc' not present (or not exposed as an output)") + self.init_rom(instructions) while self.pc <= len(instructions): @@ -618,6 +625,9 @@ def run_program(self, instructions): def reset_program(self): """Reset pc so the program will run again from the top.""" + if ('reset', 0) not in self._vector.inputs: + raise MissingComponent("No 'reset' input present") + self.reset = 1 self.ticktock() self.reset = 0 @@ -629,6 +639,9 @@ def init_rom(self, instructions): after the program, which could in theory be used to detect termination. """ + if self._rom is None: + raise MissingComponent("No ROM present") + size = len(instructions) # The ROM size limits the size of program that can run, not to mention, e.g. the format of @@ -645,27 +658,64 @@ def init_rom(self, instructions): def peek(self, address): """Read a single word from the Computer's memory.""" + + if self._mem is None: + raise MissingComponent("No RAM present") + return self._mem.storage[address] def poke(self, address, value): """Write a single word to the Computer's memory.""" + + if self._mem is None: + raise MissingComponent("No RAM present") + self._mem.storage[address] = value def peek_screen(self, address): - """Read a value from the display RAM. Address must be between 0x000 and 0x1FFF.""" + """Read a value from the separate display RAM. + + Assuming the standard memory layout, address must be between 0x000 and 0x1FFF. + """ + + if self._screen is None: + raise MissingComponent("No separate screen RAM present") + return self._screen.storage[address] def poke_screen(self, address, value): - """Write a value to the display RAM. Address must be between 0x000 and 0x1FFF.""" + """Write a value to the separate display RAM. + + Assuming the standard memory layout, address must be between 0x000 and 0x1FFF. + """ + + if self._screen is None: + raise MissingComponent("No separate screen RAM present") + self._screen.storage[address] = value + def peek_rom(self, address): + """Read a single word from the Computer's ROM.""" + + if self._rom is None: + raise MissingComponent("No ROM present") + + return self._rom.storage[address] + def set_keydown(self, keycode): """Provide the code which identifies a single key which is currently pressed.""" + + if self._keyboard is None: + raise MissingComponent("No Input present") + self._keyboard.set(keycode) def get_tty(self): """Read one word of output which has been written to the tty port, and reset it to 0.""" + if self._tty is None: + raise MissingComponent("No Output present") + # Tricky: clearing the value in the Output component flips its `ready` output, most of the time. self._vector.dirty = True return self._tty.read() @@ -674,5 +724,30 @@ def get_tty(self): # that subclasses can override. @property def sp(self): - """Read the current value of the stack pointer, which is normally stored at RAM[0].""" - return self.peek(0) + """Read the current value of the stack pointer, which is normally stored at RAM[0], but may + be an ordinary output in some cases. + """ + if "sp" in self.outputs(): + return self.__getattr__("sp") + else: + return self.peek(0) + + def __repr__(self): + return str(self.outputs()) + + +class MissingComponent(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + + +# def has_rom(ic): +# """Check for presence of exactly one ROM (any size).""" +# roms = [c for c in ic.sorted_components() if isinstance(c, ROM)] +# return len(roms) == 1 + + +# def has_ram(ic, address_bits): +# """Check for presence of exactly one RAM with the expected size.""" +# rams = [c for c in ic.sorted_components() if isinstance(c, RAM) and c.address_bits == address_bits] +# return len(rams) == 1 diff --git a/test_05.py b/test_05.py index b0c9f11..5dc6798 100755 --- a/test_05.py +++ b/test_05.py @@ -106,6 +106,7 @@ def test_memory_system(): mem.tick(); mem.tock() assert mem.out == -1 + # ...load still set mem.address = 0x504f mem.tick(); mem.tock() assert mem.out == -1 diff --git a/test_12.py b/test_12.py index a1eca98..f451734 100755 --- a/test_12.py +++ b/test_12.py @@ -198,8 +198,9 @@ def test_keyboard_lib(keyboard_class=project_12.KEYBOARD_CLASS, platform=BUNDLED def crank(cycles=100_000): """Run for a while, checking the tty for output every few cycles.""" nonlocal output - for _ in range(cycles//100): - computer.ticktock(cycles=100) + CYCLES_PER_CALL = 50 + for _ in range(cycles//50): + computer.ticktock(cycles=CYCLES_PER_CALL) computer.ticktock() c = computer.get_tty() if c != 0: