retroforth/vm/nga-swift/nga.swift
2023-01-15 22:09:35 +00:00

500 lines
9.2 KiB
Swift

//
// Nga.swift
//
// Nga is the virtual machine at the heart
// of RETRO. It provides a MISC based dual
// stack processor that's capable of hosting
// RETRO or other environments.
//
// Copyright, Charles Childers
//
import Foundation
// Limits
let imageSize: Int = 512000
let stackSize: Int = 2048
let CELLMAX = Int64.max - 1
let CELLMIN = Int64.min
// Notes:
// While Nga was designed as a 32-bit system,
// this implementation is 64-bit internally
// to allow greater numeric range. This follows
// with recent changes to the other Nga variants.
// I'm adding some extensions to existing types
// for easier access to individual characters
// in strings and bytes in integers.
extension StringProtocol {
var asciiValues: [UInt8] { compactMap(\.asciiValue) }
}
extension FixedWidthInteger where Self: SignedInteger {
var bytes: [Int8] {
var _endian = littleEndian
let bytePtr = withUnsafePointer(to: &_endian) {
$0.withMemoryRebound(to: Int8.self, capacity: MemoryLayout<Self>.size) {
UnsafeBufferPointer(start: $0, count: MemoryLayout<Self>.size)
}
}
return [Int8](bytePtr)
}
}
// I implement a stack class here for handling
// the details of the stacks. Stacks in Nga are
// LIFO, holding only signed integer values.
class Stack {
private var data = [Int64](repeating: 0, count: stackSize)
private var sp: Int = 0
public func tos() -> Int64 {
return data[sp]
}
public func nos() -> Int64 {
return data[sp - 1]
}
public func push(_ n: Int64) {
sp += 1
data[sp] = n
}
public func pop() -> Int64 {
let v = data[sp]
data[sp] = 0
sp -= 1
return v
}
public func drop() {
data[sp] = 0
sp -= 1
}
public func swap() {
let a = tos()
let b = nos()
data[sp] = b
data[sp - 1] = a
}
public func depth() -> Int {
return sp
}
public func item(_ n: Int) -> Int64 {
return data[n]
}
}
// Instruction Pointer and Memory
// I'm adding some extra padding at EOM for now.
// This may be changed in the future.
var ip: Int = 0
var memory = [Int64](repeating: 0, count: imageSize + 1024)
// Create the stacks
var data = Stack()
var address = Stack()
// image loader
func loadImage() {
let fileURL = URL(fileURLWithPath: "ngaImage")
let data = NSData(contentsOf: fileURL)!
var i: Int = 0
while (i < (data.count / 8)) {
memory[i] = getInt64FromData(data: data, offset: i)
i += 1
}
}
func getInt64FromData(data: NSData, offset: Int) -> Int64 {
let raw = NSRange(location: offset * 8, length: 8)
var i = [Int64](repeating: 0, count: 1)
data.getBytes(&i, range: raw)
return Int64(i[0])
}
// Now, I implement the instructions. Each gets
// a dedicated function.
func inst_no() {
}
func inst_li() {
ip += 1
data.push(memory[ip])
}
func inst_du() {
data.push(data.tos())
}
func inst_dr() {
data.drop()
}
func inst_sw() {
data.swap()
}
func inst_pu() {
address.push(data.pop())
}
func inst_po() {
data.push(address.pop())
}
func inst_ju() {
ip = Int(data.pop() - 1)
}
func inst_ca() {
address.push(Int64(ip))
ip = Int(data.pop() - 1)
}
func inst_cc() {
let dest = data.pop()
let flag = data.pop()
if (flag != 0) {
address.push(Int64(ip))
ip = Int(dest - 1)
}
}
func inst_re() {
ip = Int(address.pop())
}
func inst_eq() {
let tos = data.pop()
let nos = data.pop()
data.push((nos == tos) ? -1 : 0)
}
func inst_ne() {
let tos = data.pop()
let nos = data.pop()
data.push((nos != tos) ? -1 : 0)
}
func inst_lt() {
let tos = data.pop()
let nos = data.pop()
data.push((nos < tos) ? -1 : 0)
}
func inst_gt() {
let tos = data.pop()
let nos = data.pop()
data.push((nos > tos) ? -1 : 0)
}
func inst_fe() {
let target = data.pop()
switch (target) {
case -1:
data.push(Int64(data.depth()))
case -2:
data.push(Int64(address.depth()))
case -3:
data.push(Int64(imageSize))
case -4:
data.push(CELLMIN)
case -5:
data.push(CELLMAX)
default:
data.push(memory[Int(target)])
}
}
func inst_st() {
let addr = data.pop()
let value = data.pop()
memory[Int(addr)] = value;
}
func inst_ad() {
let tos = data.pop()
let nos = data.pop()
data.push(nos &+ tos)
}
func inst_su() {
let tos = data.pop()
let nos = data.pop()
data.push(nos &- tos)
}
func inst_mu() {
let tos = data.pop()
let nos = data.pop()
data.push(nos &* tos)
}
func inst_di() {
let a = data.pop()
let b = data.pop()
data.push(b % a)
data.push(b / a)
}
func inst_an() {
let tos = data.pop()
let nos = data.pop()
data.push(tos & nos)
}
func inst_or() {
let tos = data.pop()
let nos = data.pop()
data.push(nos | tos)
}
func inst_xo() {
let tos = data.pop()
let nos = data.pop()
data.push(nos ^ tos)
}
func inst_sh() {
let tos = data.pop()
let nos = data.pop()
if (tos < 0) {
data.push(nos << (tos * -1))
}
else {
if (nos < 0 && tos > 0) {
data.push(nos >> tos | ~(~0 >> tos))
}
else {
data.push(nos >> tos)
}
}
}
func inst_zr() {
if (data.tos() == 0) {
data.drop()
ip = Int(address.pop())
}
}
func inst_ha() {
ip = imageSize;
}
func inst_ie() {
data.push(1)
}
func inst_iq() {
data.drop()
data.push(0)
data.push(0)
}
func inst_ii() {
data.drop()
let c = Int(data.pop())
let v = UnicodeScalar(c) ?? UnicodeScalar(32)
print(Character(v!), terminator: "")
}
// With those done, I turn to handling the
// instructions.
// In C, I use a jump table for this. I don't
// know how to do this in Swift.
func ngaProcessOpcode(opcode: Int64) {
switch opcode {
case 0:
()
case 1:
inst_li()
case 2:
inst_du()
case 3:
inst_dr()
case 4:
inst_sw()
case 5:
inst_pu()
case 6:
inst_po()
case 7:
inst_ju()
case 8:
inst_ca()
case 9:
inst_cc()
case 10:
inst_re()
case 11:
inst_eq()
case 12:
inst_ne()
case 13:
inst_lt()
case 14:
inst_gt()
case 15:
inst_fe()
case 16:
inst_st()
case 17:
inst_ad()
case 18:
inst_su()
case 19:
inst_mu()
case 20:
inst_di()
case 21:
inst_an()
case 22:
inst_or()
case 23:
inst_xo()
case 24:
inst_sh()
case 25:
inst_zr()
case 26:
inst_ha()
case 27:
inst_ie()
case 28:
inst_iq()
case 29:
inst_ii()
default:
inst_ha()
}
}
// Each opcode can have up to four instructions
// This validates them, letting us know if it's
// safe to run them.
func ngaValidatePackedOpcodes(_ opcode: Int64) -> Bool {
var valid = true
for inst in Int32(opcode).bytes {
if !(inst >= 0 && inst <= 29) {
valid = false
}
}
return valid
}
// This will process an opcode bundle
func ngaProcessPackedOpcodes(_ opcode: Int64) {
for inst in Int64(opcode).bytes {
ngaProcessOpcode(opcode: Int64(inst))
}
}
// For debugging purposes
func dump() {
print("----------------------------")
for i in 0 ... data.depth() {
if i == data.depth() {
print("TOS", terminator: " ")
}
print(Int(data.item(i)))
}
print("----------------------------")
}
// Interfacing
func extractString(at: Int) -> String {
var s: String = ""
var next = at
while memory[next] != 0 {
let value = Int(memory[next])
let u = UnicodeScalar(value)
let char = Character(u!)
s.append(char)
next += 1
}
return s
}
func injectString(_ s: String, to: Int) {
var i: Int = 0
for ch in s.asciiValues {
let n = Int64(ch)
memory[to + i] = n
memory[to + i + 1] = 0
i += 1
}
}
// Run through an image, quiting when EOM
// is reached
func execute(_ word: Int) {
address.push(0)
ip = word
while ip < imageSize {
// print("ip:", ip, "bundle", memory[ip], "insts", Int32(memory[ip]).bytes)
let opcode = memory[ip]
if ngaValidatePackedOpcodes(opcode) {
ngaProcessPackedOpcodes(opcode)
} else {
print("Error: invalid opcode")
ip = imageSize
}
ip += 1
if address.depth() == 0 {
ip = imageSize
}
}
}
func process() {
ip = 0
while ip < imageSize - 1 {
ngaProcessPackedOpcodes(memory[ip])
ip += 1
}
}
// Tests
loadImage()
let interpret: Int = Int(memory[5])
var done = false
print("RETRO (using nga.swift)")
while !done {
let code = readLine() ?? "()"
if code == "bye" {
done = true
} else {
let tokens = code.components(separatedBy: " ")
for token in tokens {
injectString(token, to: Int(memory[7]))
data.push(memory[7])
execute(interpret)
}
}
}
dump()