// // 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.size) { UnsafeBufferPointer(start: $0, count: MemoryLayout.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()