diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf8b0e9 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +SallyForth: A simple Forth-like language implemented in Python. +############################################################## + +SallyForth is a simple hobby implementation of the FORTH programming +language. Possibly the most interesting thing about SallyForth is the +name, which +[Michael Nygard suggested](https://twitter.com/mtnygard/status/1249781530219642883) +as a name for a FORTH implementation +at exactly the same time that I happened to be writing this code. diff --git a/sallyforth/compiler.py b/sallyforth/compiler.py new file mode 100644 index 0000000..24f4572 --- /dev/null +++ b/sallyforth/compiler.py @@ -0,0 +1,28 @@ +from lex import forth_prompt +from stack import Stack + +class Compiler: + def __init__(self, name=None): + self.name = name + self.instructions = [] + self.offsets = Stack() + + def add_instruction(self, ins): + self.instructions.append(ins) + + def offset(self): + return len(self.instructions) + + def push_offset(self): + self.offsets.push(self.offset()) + + def pop_offset(self): + return self.offsets.pop() + + def _str__(self): + result = f'Compiler {name} {immediate} ' + for i in self.instructions: + result += str(i) + result += ' ' + return result + diff --git a/sallyforth/kernel.py b/sallyforth/kernel.py new file mode 100644 index 0000000..3f512b3 --- /dev/null +++ b/sallyforth/kernel.py @@ -0,0 +1,115 @@ +import sys +from os import path +from words import * +from lex import forth_prompt, read_tokens, is_string +from stack import Stack + +def to_number(token): + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return None + +def push_value_f(value): + def x(f): + f.stack.push(value) + return 1 + return x + +class Forth: + def __init__(self, startup=None): + self.stack = Stack() + self.dictionary = { + 'true': const_f(True), + 'false': const_f(False), + 'nil': const_f(None), + '0': const_f(0), + '1': const_f(1), + '2': const_f(2), + ';': w_semi, + ':': w_colon, + '+': w_add, + '+': w_add, + '-': w_sub, + '/': w_div, + '>': w_gt, + '<': w_lt, + '<=': w_le, + '>=': w_ge, + '=': w_eq, + 'dup': w_dup, + 'swap': w_swap, + '.': w_dot, + 'nl': w_nl, + 'dump': w_dump, + 'idump': w_idump, + 'stack': w_stack, + 'begin': w_begin, + 'until': w_until, + 'if': w_if, + 'then': w_then} + + self.compiler = None + if startup: + execute_startup(startup) + + def compiling(self): + return self.compiler + + def process_line(self, readline_f=forth_prompt): + tokens = read_tokens(readline_f) + for token in tokens: + if not self.compiling(): + self.interpret_token(token) + else: + self.compile_token(token) + + def compile_token(self, token): + if self.compiler.name == None: + self.compiler.name = token + return + + if is_string(token): + self.compiler.add_instruction(push_value_f(token[1::])) + return + + if token in self.dictionary: + word = self.dictionary[token] + if 'immediate' in word.__dict__: + #print("before immediate word:", self, self.dictionary) + word(self) + #print("after immediate word:", self, self.dictionary) + else: + self.compiler.add_instruction(self.dictionary[token]) + return + + n = to_number(token) + if n == None: + self.compiler = None + print(token, "? Compile terminated.") + else: + self.compiler.add_instruction(push_value_f(n)) + + def interpret_token(self, token): + if is_string(token): + self.stack.push(token) + return + + if token in self.dictionary: + self.dictionary[token](self) + return + + n = to_number(token) + if n == None: + print(token, "?") + else: + self.stack.push(n) + + def dump(self): + print("Forth:", self) + print("Stack:", self.stack) + print("Dictionary:", self.dictionary) + print("Compiler:", self.compiler) diff --git a/sallyforth/lex.py b/sallyforth/lex.py new file mode 100644 index 0000000..b223750 --- /dev/null +++ b/sallyforth/lex.py @@ -0,0 +1,55 @@ +import sys +from os import path + +def is_string(token): + #print("is string:", token, token[0], token[0] == '"') + return token[0] == '"' + +def is_space(ch): + return ch == " " or ch == "\t" or ch == "\n" + +def tokenize(s): + state = 'start' + token = '' + tokens = [] + for ch in s: + #print(f'Loop state {state} token {token} ch {ch}') + if state == 'start' and is_space(ch): + continue + elif state == 'start' and ch == '"': + token = ch + state = 'string' + elif state == 'start': + token = ch + state = 'word' + elif state == 'string' and ch == '"': + tokens.append(token) + state = 'start' + token = '' + elif state == 'word' and is_space(ch): + tokens.append(token) + state = 'start' + token = '' + elif state == 'word' or state == 'string': + token += ch + else: + print(f'State: [{state}] token: [{token}] ch: [{ch}]???') + state = 'start' + if len(token) > 0: + tokens.append(token) + return tokens + +def read_ch(): + return sys.stdin.read(1) + +def read_tokens(read_f): + line = read_f() + return tokenize(line) + +def forth_prompt(): + return input("SallyForth>> ") + +def file_read_f(f): + def read_it(): + return f.readline() + return read_it diff --git a/sallyforth/sallyforth.py b/sallyforth/sallyforth.py new file mode 100644 index 0000000..c9fa480 --- /dev/null +++ b/sallyforth/sallyforth.py @@ -0,0 +1,6 @@ +from kernel import Forth + +f = Forth() +while True: + f.process_line() + diff --git a/sallyforth/stack.py b/sallyforth/stack.py new file mode 100644 index 0000000..ca27995 --- /dev/null +++ b/sallyforth/stack.py @@ -0,0 +1,29 @@ +class Stack: + def __init__(self): + self.top = -1 + self.stack = 100 * [None] + + def push(self, x): + # print("stack push", x) + self.top += 1 + self.stack[self.top] = x + return x + + def pop(self): + result = self.stack[self.top] + # print("stack pop", result) + self.top -= 1 + if self.top < -1: + print("stack overpop") + self.top = -1; + return result + + def peek(self): + return self.stack[self.top] + + def __str__(self): + result = '' + for i in range(self.top + 1): + result += str(self.stack[i]) + result += ' ' + return result diff --git a/sallyforth/words.py b/sallyforth/words.py new file mode 100644 index 0000000..8426fe7 --- /dev/null +++ b/sallyforth/words.py @@ -0,0 +1,200 @@ +from compiler import Compiler + +def execute_f(name, instructions): + # print("execute_f:", len(instructions)) + def inner(forth, debug=False): + # print("inner f:", name) + # print("inner f:", len(instructions)) + i = 0 + while i >= 0: + # print(i, "=>", instructions[i]) + delta = instructions[i](forth) + i += delta + return 1 + return inner + +def ifnot_jump_f(n): + def ifnot_jump(forth): + # print("If not jump:") + x = forth.stack.pop() + # print("==>value:", x) + if not x: + # print("==>", x, " is false") + # print("==>returning", n) + return n + # print("==>returning 1") + return 1 + return ifnot_jump + +def const_f(value): + def x(f): + f.stack.push(value) + return 1 + return x + +def w_gt(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b > a) + return 1 + +def w_lt(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b < a) + return 1 + +def w_eq(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(a==b) + return 1 + +def w_le(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b<=a) + return 1 + +def w_ge(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b>=a) + return 1 + +def w_add(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b+a) + return 1 + +def w_mul(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b*a) + return 1 + +def w_sub(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b-a) + return 1 + +def w_div(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(b/a) + return 1 + +def w_dot(f): + a = f.stack.pop() + print(a, end='') + return 1 + +def w_dup(f): + x = f.stack.peek() + f.stack.push(x) + return 1 + +def w_swap(f): + a = f.stack.pop() + b = f.stack.pop() + f.stack.push(a) + f.stack.push(b) + return 1 + +def w_nl(f): + print() + return 1 + +def w_return(f): + return -9999999999 + +def w_colon(f): + f.compiler = Compiler() + +def w_semi(forth): + forth.compiler.add_instruction(w_return) + name = forth.compiler.name + word_f = execute_f(name, forth.compiler.instructions) + forth.dictionary[name] = word_f + forth.compiler = None + return 1 + +w_semi.__dict__['immediate'] = True + +def w_should_not_happen(forth): + print("Should not execute this word!") + raise ValueError + +def w_if(forth): + print("w_if") + compiler = forth.compiler + compiler.push_offset() + compiler.add_instruction(w_should_not_happen) + return 1 + +w_if.__dict__['immediate'] = True + +def w_then(forth): + compiler = forth.compiler + if_offset = compiler.pop_offset() + end_offset = compiler.offset() + delta = end_offset - if_offset + compiler.instructions[if_offset] = ifnot_jump_f(delta) + return 1 + +w_then.__dict__['immediate'] = True + +def w_do(forth): + print("w_do") + compiler = forth.compiler + compiler.push_offset() + compiler.add_instruction(w_should_not_happen) + return 1 + +w_do.__dict__['immediate'] = True + +def w_while(forth): + compiler = forth.compiler + do_offset = compiler.pop_offset() + while_offset = compiler.offset() + delta = do_offset - while_offset + compiler.instructions[if_offset] = ifnot_jump_f(delta) + return 1 + +w_while.__dict__['immediate'] = True + +def w_begin(forth): + compiler = forth.compiler + compiler.push_offset() + return 1 + +w_begin.__dict__['immediate'] = True + +def w_until(forth): + compiler = forth.compiler + begin_offset = compiler.pop_offset() + until_offset = compiler.offset() + delta = begin_offset - until_offset + print("Delta:", delta) + compiler.instructions.append(ifnot_jump_f(delta)) + return 1 + + +w_until.__dict__['immediate'] = True + +def w_dump(f): + f.dump() + return 1 + +def w_idump(f): + f.dump() + return 1 + +w_idump.__dict__['immediate'] = True + +def w_stack(f): + print(f.stack) + return 1 +