retroforth/tools/retro-extend.py
crc ce91454df1 add in a "d:source" field to the dictionary header
FossilOrigin-Name: d6c27c49b054c022fad6d2898d173f0a2705925a97bb139c1c6fbdb48f896da7
2021-08-10 16:09:58 +00:00

823 lines
24 KiB
Python
Executable file

#!/usr/bin/env python3
# Nga: a Virtual Machine
# Copyright (c) 2010 - 2020, Charles Childers
# Floating Point I/O by Arland Childers, (c) 2020
# Optimizations and process() rewrite by Greg Copeland
# -------------------------------------------------------------
# This implementation of the VM differs from the reference
# model in a number of important ways.
#
# To aid in performance, it does the following:
# - caching the Retro dictionary in a Python dict()
# - replaces some Retro words with implementations in Python
# - s:eq?
# - s:length
# - s:to-number
# - d:lookup
#
# Each major component is managed as a separate class. We have
# a class for each I/O device, for each stack, and for the
# memory pool. The main VM is also in a separate class.
#
# It's intended that an amalgamation tool will be developed to
# combine the separate files into a single one for deployment.
# -------------------------------------------------------------
import os, sys, math, time, struct, random, datetime
class Clock:
def __getitem__(self, id):
import datetime
import time
now = datetime.datetime.now()
ids = {
"time": time.time,
"year": now.year,
"month": now.month,
"day": now.day,
"hour": now.hour,
"minute": now.minute,
"second": now.second,
# No time_utc?
"year_utc": now.utcnow().year,
"month_utc": now.utcnow().month,
"day_utc": now.utcnow().day,
"hour_utc": now.utcnow().hour,
"minute_utc": now.utcnow().minute,
"second_utc": now.utcnow().second,
}
return ids[id]
import random
class RNG:
def __call__(self):
return random.randint(-2147483647, 2147483646)
import os
class FileSystem:
def __init__(self):
self.files = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def open(self, params):
name, mode = params
slot = 0
i = 1
while i < 8:
if self.files[i] == 0:
slot = i
i += 1
if slot > 0:
if mode == 0:
if os.path.exists(name):
self.files[slot] = open(name, "r")
else:
slot = 0
elif mode == 1:
self.files[slot] = open(name, "w")
elif mode == 2:
self.files[slot] = open(name, "a")
elif mode == 3:
if os.path.exists(name):
self.files[slot] = open(name, "r+")
else:
slot = 0
return slot
def read(self, slot):
return ord(self.files[slot].read(1))
def write(self, params):
slot, char = params
self.files[slot].write(chr(stack.pop()))
return 1
def close(self, slot):
self.files[slot].close()
self.files[slot] = 0
return 0
def pos(self, slot):
return self.files[slot].tell()
def seek(slot, pos):
return self.files[slot].seek(pos, 0)
def size(self, slot):
at = self.files[slot].tell()
self.files[slot].seek(0, 2) # SEEK_END
end = self.files[slot].tell()
self.files[slot].seek(at, 0) # SEEK_SET
return end
def delete(self, name):
name = extract_string(stack.pop())
i = 0
if os.path.exists(name):
os.remove(name)
i = 1
return i
class FloatStack(object):
def __init__(self, *d):
self.data = list(d)
def __getitem__(self, id):
return self.data[id]
def add(self):
self.data.append(self.data.pop() + self.data.pop())
def sub(self):
self.data.append(0 - (self.data.pop() - self.data.pop()))
def mul(self):
self.data.append(self.data.pop() * self.data.pop())
def div(self):
a, b = self.data.pop(), self.data.pop()
self.data.append(b / a)
def ceil(self):
self.data.append(math.ceil(self.data.pop()))
def floor(self):
self.data.append(math.floor(self.data.pop()))
def eq(self):
return 0 - (self.data.pop() == self.data.pop())
def neq(self):
return 0 - (self.data.pop() != self.data.pop())
def gt(self):
a, b = self.data.pop(), self.data.pop()
return 0 - (b > a)
def lt(self):
a, b = self.data.pop(), self.data.pop()
return 0 - (b < a)
def depth(self):
return len(self.data)
def drop(self):
self.data.pop()
def pop(self):
return self.data.pop()
def swap(self):
a, b = self.data.pop(), self.data.pop()
self.data += [a, b]
def push(self, n):
self.data.append(n)
def log(self):
a, b = self.data.pop(), self.data.pop()
self.data.append(math.log(b, a))
def power(self):
a, b = self.data.pop(), self.data.pop()
self.data.append(math.pow(a, b))
def sin(self):
self.data.append(math.sin(self.data.pop()))
def cos(self):
self.data.append(math.cos(self.data.pop()))
def tan(self):
self.data.append(math.tan(self.data.pop()))
def asin(self):
self.data.append(math.asin(self.data.pop()))
def acos(self):
self.data.append(math.acos(self.data.pop()))
def atan(self):
self.data.append(math.atan(self.data.pop()))
class IntegerStack(list):
def __init__(self):
stack = [] * 128
self.extend(stack)
def depth(self):
return len(self)
def tos(self):
return self[-1]
def push(self, v):
self.append(v)
def dup(self):
self.append(self[-1])
def drop(self):
self.pop()
def swap(self):
a = self[-2]
self[-2] = self[-1]
self[-1] = a
import os
import struct
class Memory(list):
def __init__(self, source, initial, size):
m = [0] * size
self.extend(m)
if len(initial) == 0:
cells = int(os.path.getsize(source) / 4)
f = open(source, "rb")
i = 0
for cell in list(struct.unpack(cells * "i", f.read())):
self[i] = cell
i = i + 1
f.close()
else:
i = 0
for cell in initial:
self[i] = cell
i = i + 1
def load_image(self, name):
cells = int(os.path.getsize(name) / 4)
f = open(name, "rb")
i = 0
for cell in list(struct.unpack(cells * "i", f.read())):
self[i] = cell
i = i + 1
f.close()
def size(self):
return len(self)
InitialImage = []
class Retro:
def map_in(self, name):
return self.memory[self.find_entry(name) + 1]
def __init__(self):
self.ip = 0
self.stack = IntegerStack()
self.address = IntegerStack()
self.memory = Memory("ngaImage", InitialImage, 1000000)
self.clock = Clock()
self.rng = RNG()
self.files = FileSystem()
self.floats = FloatStack()
self.afloats = FloatStack()
self.Dictionary = self.populate_dictionary()
self.Cached = self.cache_words()
self.setup_devices()
self.instructions = [
self.i_nop,
self.i_lit,
self.i_dup,
self.i_drop,
self.i_swap,
self.i_push,
self.i_pop,
self.i_jump,
self.i_call,
self.i_ccall,
self.i_return,
self.i_eq,
self.i_neq,
self.i_lt,
self.i_gt,
self.i_fetch,
self.i_store,
self.i_add,
self.i_subtract,
self.i_multiply,
self.i_divmod,
self.i_and,
self.i_or,
self.i_xor,
self.i_shift,
self.i_zreturn,
self.i_halt,
self.i_ienumerate,
self.i_iquery,
self.i_iinvoke,
]
def div_mod(self, a, b):
x = abs(a)
y = abs(b)
q, r = divmod(x, y)
if a < 0 and b < 0:
r *= -1
elif a > 0 and b < 0:
q *= -1
elif a < 0 and b > 0:
r *= -1
q *= -1
return q, r
def cache_words(self):
Cached = dict()
Cached["interpreter"] = self.map_in("interpret")
Cached["not_found"] = self.map_in("err:notfound")
Cached["s:eq?"] = self.map_in("s:eq?")
Cached["s:to-number"] = self.map_in("s:to-number")
Cached["s:length"] = self.map_in("s:length")
Cached["d:lookup"] = self.map_in("d:lookup")
Cached["d:add-header"] = self.map_in("d:add-header")
return Cached
def populate_dictionary(self):
Dictionary = dict()
header = self.memory[2]
while header != 0:
named = self.extract_string(header + 4)
if not named in Dictionary:
Dictionary[named] = header
header = self.memory[header]
return Dictionary
def find_entry(self, named):
if named in self.Dictionary:
return self.Dictionary[named]
header = self.memory[2]
Done = False
while header != 0 and not Done:
if named == self.extract_string(header + 4):
self.Dictionary[named] = header
Done = True
else:
header = self.memory[header]
return header
def get_input(self):
return ord(sys.stdin.read(1))
def display_character(self):
if self.stack.tos() > 0 and self.stack.tos() < 128:
if self.stack.tos() == 8:
sys.stdout.write(chr(self.stack.pop()))
sys.stdout.write(chr(32))
sys.stdout.write(chr(8))
else:
sys.stdout.write(chr(self.stack.pop()))
else:
sys.stdout.write("\033[2J\033[1;1H")
self.stack.pop()
sys.stdout.flush()
def i_nop(self):
pass
def i_lit(self):
self.ip += 1
self.stack.push(self.memory[self.ip])
def i_dup(self):
self.stack.dup()
def i_drop(self):
self.stack.drop()
def i_swap(self):
self.stack.swap()
def i_push(self):
self.address.push(self.stack.pop())
def i_pop(self):
self.stack.push(self.address.pop())
def i_jump(self):
self.ip = self.stack.pop() - 1
def i_call(self):
self.address.push(self.ip)
self.ip = self.stack.pop() - 1
def i_ccall(self):
target = self.stack.pop()
if self.stack.pop() != 0:
self.address.push(self.ip)
self.ip = target - 1
def i_return(self):
self.ip = self.address.pop()
def i_eq(self):
a = self.stack.pop()
b = self.stack.pop()
if b == a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_neq(self):
a = self.stack.pop()
b = self.stack.pop()
if b != a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_lt(self):
a = self.stack.pop()
b = self.stack.pop()
if b < a:
self.stack.push(-1)
else:
self.stack.push(0)
def i_gt(self):
a = self.stack.pop()
b = self.stack.pop()
if b > a:
self.stack.push(-1)
else:
self.stack.push(0)
# The fetch instruction also handles certain
# introspection queries.
#
# Of note is the min and max values for a cell.
# In most VM implementations, this is limited
# to 32 bit or 64 bit ranges, but Python allows
# an unlimited range.
#
# I report as if the cells are capped at 128 bits
# but you can safely ignore this if running on
# the Python VM. (This does have an impact on
# floating point values, if using the `e:` words
# for converting them to/from an encoded format in
# standard cells, but should not affect anything
# else in the standard system)
def i_fetch_query(self, target):
if target == -1:
self.stack.push(self.stack.depth())
elif target == -2:
self.stack.push(self.address.depth())
elif target == -3:
self.stack.push(self.memory.size())
elif target == -4:
self.stack.push(-2147483647)
elif target == -5:
self.stack.push(2147483646)
else:
raise IndexError
def i_fetch(self):
target = self.stack.pop()
if target >= 0:
self.stack.push(self.memory[target])
else:
self.i_fetch_query(target)
def i_store(self):
mi = self.stack.pop()
self.memory[mi] = self.stack.pop()
def i_add(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(t + v)
def i_subtract(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(v - t)
def i_multiply(self):
t = self.stack.pop()
v = self.stack.pop()
self.stack.push(v * t)
def i_divmod(self):
t = self.stack.pop()
v = self.stack.pop()
b, a = self.div_mod(v, t)
self.stack.push(a)
self.stack.push(b)
def i_and(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m & t)
def i_or(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m | t)
def i_xor(self):
t = self.stack.pop()
m = self.stack.pop()
self.stack.push(m ^ t)
def i_shift(self):
t = self.stack.pop()
v = self.stack.pop()
if t < 0:
v <<= t * -1
else:
v >>= t
self.stack.push(v)
def i_zreturn(self):
if self.stack.tos() == 0:
self.stack.pop()
self.ip = self.address.pop()
def i_halt(self):
self.ip = 9000000
def i_ienumerate(self):
self.stack.push(6)
def i_iquery(self):
device = self.stack.pop()
if device == 0: # generic output
self.stack.push(0)
self.stack.push(0)
if device == 1: # floating point
self.stack.push(1)
self.stack.push(2)
if device == 2: # files
self.stack.push(0)
self.stack.push(4)
if device == 3: # rng
self.stack.push(0)
self.stack.push(10)
if device == 4: # time
self.stack.push(0)
self.stack.push(5)
if device == 5: # scripting
self.stack.push(0)
self.stack.push(9)
def file_open_params(self):
mode = self.stack.pop()
name = self.extract_string(self.stack.pop())
return name, mode
def file_write_params(self):
slot = self.stack.pop()
char = self.stack.pop()
return slot, char
def setup_devices(self):
self.files_instr = {
0: lambda: self.stack.push(self.files.open(self.file_open_params())),
1: lambda: self.files.close(self.stack.pop()),
2: lambda: self.stack.push(self.files.read(self.stack.pop())),
3: lambda: self.files.write(self.file_write_params()),
4: lambda: self.stack.push(self.files.pos(self.stack.pop())),
5: lambda: self.files.seek(),
6: lambda: self.stack.push(self.files.size(self.stack.pop())),
7: lambda: self.files.delete(self.extract_string(self.stack.pop())),
8: lambda: 1 + 1,
}
self.rng_instr = {0: lambda: self.stack.push(self.rng())}
self.clock_instr = {
0: lambda: self.stack.push(int(time.time())),
1: lambda: self.stack.push(self.clock["day"]),
2: lambda: self.stack.push(self.clock["month"]),
3: lambda: self.stack.push(self.clock["year"]),
4: lambda: self.stack.push(self.clock["hour"]),
5: lambda: self.stack.push(self.clock["minute"]),
6: lambda: self.stack.push(self.clock["second"]),
7: lambda: self.stack.push(self.clock["day_utc"]),
8: lambda: self.stack.push(self.clock["month_utc"]),
9: lambda: self.stack.push(self.clock["year_utc"]),
10: lambda: self.stack.push(self.clock["hour_utc"]),
11: lambda: self.stack.push(self.clock["minute_utc"]),
12: lambda: self.stack.push(self.clock["second_utc"]),
}
self.float_instr = {
0: lambda: self.floats.push(float(self.stack.pop())),
1: lambda: self.floats.push(float(self.extract_string(self.stack.pop()))),
2: lambda: self.stack.push(int(self.floats.pop())),
3: lambda: self.inject_string(str(self.floats.pop()), self.stack.pop()),
4: lambda: self.floats.add(),
5: lambda: self.floats.sub(),
6: lambda: self.floats.mul(),
7: lambda: self.floats.div(),
8: lambda: self.floats.floor(),
9: lambda: self.floats.ceil(),
10: lambda: self.floats.sqrt(),
11: lambda: self.stack.push(self.floats.eq()),
12: lambda: self.stack.push(self.floats.neq()),
13: lambda: self.stack.push(self.floats.lt()),
14: lambda: self.stack.push(self.floats.gt()),
15: lambda: self.stack.push(self.floats.depth()),
16: lambda: self.floats.dup(),
17: lambda: self.floats.drop(),
18: lambda: self.floats.swap(),
19: lambda: self.floats.log(),
20: lambda: self.floats.pow(),
21: lambda: self.floats.sin(),
22: lambda: self.floats.cos(),
23: lambda: self.floats.tan(),
24: lambda: self.floats.asin(),
25: lambda: self.floats.atan(),
26: lambda: self.floats.acos(),
27: lambda: self.afloats.push(self.floats.pop()),
28: lambda: self.floats.push(self.afloats.pop()),
29: lambda: self.stack.push(self.afloats.depth()),
}
def i_iinvoke(self):
device = self.stack.pop()
# print('dev:', device)
if device == 0:
self.display_character()
if device == 1:
action = self.stack.pop()
self.float_instr[int(action)]()
if device == 2:
action = self.stack.pop()
self.files_instr[int(action)]()
if device == 3:
self.rng_instr[0]()
if device == 4:
action = self.stack.pop()
self.clock_instr[int(action)]()
if device == 5:
action = self.stack.pop()
if action == 0:
self.stack.push(len(sys.argv) - 2)
if action == 1:
a = self.stack.pop()
b = self.stack.pop()
self.stack.push(self.inject_string(sys.argv[a + 2], b))
if action == 2:
self.run_file(self.extract_string(self.stack.pop()))
if action == 3:
b = self.stack.pop()
self.stack.push(self.inject_string(sys.argv[0], b))
def validate_opcode(self, I0, I1, I2, I3):
if (
(I0 >= 0 and I0 <= 29)
and (I1 >= 0 and I1 <= 29)
and (I2 >= 0 and I2 <= 29)
and (I3 >= 0 and I3 <= 29)
):
return True
else:
return False
def extract_string(self, at):
s = ""
while self.memory[at] != 0:
s = s + chr(self.memory[at])
at = at + 1
return s
def inject_string(self, s, to):
for c in s:
self.memory[to] = ord(c)
to = to + 1
self.memory[to] = 0
def execute(self, word, notfound):
self.ip = word
if self.address.depth() == 0:
self.address.push(0)
while self.ip < 1000000:
if self.ip == self.Cached["s:eq?"]:
a = self.extract_string(self.stack.pop())
b = self.extract_string(self.stack.pop())
if a == b:
self.stack.push(-1)
else:
self.stack.push(0)
self.ip = self.address.pop()
elif self.ip == self.Cached["d:lookup"]:
name = self.extract_string(self.stack.pop())
header = self.find_entry(name)
self.stack.push(header)
self.memory[self.Cached["d:lookup"] - 20] = header # "which"
self.ip = self.address.pop()
elif self.ip == self.Cached["s:to-number"]:
n = self.extract_string(self.stack.pop())
self.stack.push(int(n))
self.ip = self.address.pop()
elif self.ip == self.Cached["s:length"]:
n = len(self.extract_string(self.stack.pop()))
self.stack.push(n)
self.ip = self.address.pop()
else:
if self.ip == notfound:
print("ERROR: word not found!")
if self.ip == self.Cached["d:add-header"]:
self.Dictionary[self.extract_string(self.stack[-3])] = self.memory[
3
]
opcode = self.memory[self.ip]
I0 = opcode & 0xFF
I1 = (opcode >> 8) & 0xFF
I2 = (opcode >> 16) & 0xFF
I3 = (opcode >> 24) & 0xFF
if self.validate_opcode(I0, I1, I2, I3):
# print("Bytecode: ", I0, I1, I2, I3, "at", self.ip)
if I0 != 0:
self.instructions[I0]()
if I1 != 0:
self.instructions[I1]()
if I2 != 0:
self.instructions[I2]()
if I3 != 0:
self.instructions[I3]()
else:
print("Invalid Bytecode: ", opcode, "at", self.ip)
self.ip = 2000000
if self.address.depth() == 0:
self.ip = 2000000
self.ip = self.ip + 1
return
def run(self):
done = False
while not done:
line = input("\nOk> ")
if line == "bye":
done = True
else:
for token in line.split():
self.inject_string(token, self.memory[7])
self.stack.push(self.memory[7])
self.execute(self.Cached["interpreter"], self.Cached["not_found"])
def run_file(self, file):
if not os.path.exists(file):
print("File '{0}' not found".format(file))
return
in_block = False
with open(file, "r") as source:
for line in source.readlines():
if line.rstrip() == "~~~":
in_block = not in_block
elif in_block:
for token in line.strip().split():
self.inject_string(token, self.memory[7])
self.stack.push(self.memory[7])
self.execute(
self.Cached["interpreter"], self.Cached["not_found"]
)
def update_image(self):
import requests
import shutil
data = requests.get("https://forthworks.com/retro/ngaImage", stream=True)
with open("ngaImage", "wb") as f:
data.raw.decode_content = True
shutil.copyfileobj(data.raw, f)
def save_image(self):
print("Writing {0} cells to {1}".format(self.memory[3], sys.argv[1]))
with open(sys.argv[1], "wb") as file:
j = 0
while j <= self.memory[3]:
cell = struct.unpack(
"=l", struct.pack("=L", self.memory[j] & 0xFFFFFFFF)
)[0]
file.write(struct.pack("i", cell))
j = j + 1
if __name__ == "__main__":
retro = Retro()
for f in sys.argv[2:]:
retro.run_file(f)
retro.save_image()