Evgenii Akentev
·
2025-11-01
root.zig
1//! By convention, root.zig is the root source file when making a library.
2const std = @import("std");
3
4// Implementation of the Little Computer 3 following
5// https://www.andreinc.net/2021/12/01/writing-a-simple-vm-in-less-than-125-lines-of-c
6
7const pcStart: u16 = 0x3000;
8const u16Max: u32 = 65535;
9pub var memory: [u16Max + 1]u16 = .{0} ** (u16Max + 1);
10var running: bool = true;
11
12inline fn mr(address: u16) u16 { return memory[address]; }
13
14inline fn mw(address: u16, value: u16) void {
15 memory[address] = value;
16}
17
18const Register = enum(u16) {
19 R0 = 0,
20 R1 = 1,
21 R2 = 2,
22 R3 = 3,
23 R4 = 4,
24 R5 = 5,
25 R6 = 6,
26 R7 = 7,
27 RPC = 8,
28 RCND = 9,
29 RCNT = 10
30};
31
32var registers: [11]u16 = .{0} ** 11;
33
34// The instruction fits into u16:
35//
36// [ OP CODE ][ PARAM1 ][ PARAM2 ][ ][ PARAM3 ]
37// [ 4 bits ][ 12 bits ]
38
39const Opcode = enum {
40 br, // conditional
41 add, // addition
42 ld, // load RPC + offset
43 st, // store
44 jsr, // jump to subroutine
45 andop, // bitwise and
46 ldr, // load base + offset
47 str, // store base + offset
48 rti, // return from interrupt
49 not, // bitwise complement
50 ldi, // load indirect
51 sti, // store indirect
52 jmp, // jump/return to subroutine
53 unused, // unused opcode
54 lea, // load effective address
55 trap, // system trap/call
56
57 pub fn parse(instruction: u16) Opcode {
58 // shift 12 bits to the right to get first 4 bits
59 return @enumFromInt(instruction >> 12);
60 }
61};
62
63const TrapFunc = enum {
64 tgetc, tout, tputs, tin, tputsp, thalt, tinu16, toutu16,
65
66 pub fn parse(n: u16) TrapFunc {
67 return @enumFromInt(n);
68 }
69};
70
71const Flag = enum(u16) {
72 FP = (1 << 0),
73 FZ = (1 << 1),
74 FN = (1 << 2),
75};
76
77inline fn uf(r: u16) void {
78 if (registers[r] == 0) { registers[@intFromEnum(Register.RCND)] = @intFromEnum(Flag.FZ); }
79 else if ((registers[r] >> 15) == 1) { registers[@intFromEnum(Register.RCND)] = @intFromEnum(Flag.FN); }
80 else registers[@intFromEnum(Register.RCND)] = @intFromEnum(Flag.FP);
81}
82
83inline fn fcnd(instruction: u16) u16 { return (instruction >> 9) & 0x7; }
84inline fn dr(instruction: u16) u16 { return (instruction >> 9) & 0x7; }
85inline fn sr1(instruction: u16) u16 { return(instruction >> 6) & 0x7; }
86inline fn sr2(instruction: u16) u16 { return (instruction) & 0x7; }
87inline fn imm(instruction: u16) u16 { return (instruction) & 0x1F; }
88inline fn fimm(instruction: u16) u16 { return (instruction >> 5) & 1; }
89
90inline fn sext(n: u16, b: u32) u16 {
91 return if ((n >> (b - 1)) & 1 == 1) @truncate(@as(u32, n) | (0xFFFF << b)) else n;
92}
93
94// 0000 0000 0001 0110 <--- a in binary
95// 1111 1111 1111 0110 <--- sextimm(a) in binary
96inline fn sextimm(instruction: u16) u16 { return sext(imm(instruction), 5); }
97
98inline fn poff(instruction: u16) u16 { return sext(instruction & 0x3F, 6); }
99inline fn poff9(instruction: u16) u16 { return sext(instruction & 0x1FF, 9); }
100inline fn poff11(instruction: u16) u16 { return sext(instruction & 0x7FF, 11); }
101inline fn fl(instruction: u16) u16 { return (instruction >> 11) & 1; }
102inline fn br(instruction: u16) u16 { return (instruction >> 6) & 0x7; }
103inline fn trp(instruction: u16) u16 { return instruction & 0xFF; }
104
105pub fn evaluate(allocator: std.mem.Allocator, i: u16) !void {
106 switch (Opcode.parse(i)) {
107 inline .add => {
108 registers[dr(i)] = registers[sr1(i)] +
109 if (fimm(i) == 1)
110 sextimm(i)
111 else registers[sr2(i)];
112 uf(dr(i));
113 },
114 inline .andop => {
115 registers[dr(i)] = registers[sr1(i)] & (if (fimm(i) == 1) (sextimm(i)) else registers[sr2(i)]);
116 uf(dr(i));
117 },
118 inline .ld => {
119 registers[dr(i)] = mr(registers[@intFromEnum(Register.RPC)] + poff9(i));
120 uf(dr(i));
121 },
122 inline .ldi => {
123 registers[dr(i)] = mr(mr(registers[@intFromEnum(Register.RPC)] + poff9(i)));
124 uf(dr(i));
125 },
126 inline .ldr => {
127 registers[dr(i)] = mr(registers[sr1(i)] + poff(i));
128 uf(dr(i));
129 },
130 inline .lea => {
131 registers[dr(i)] = registers[@intFromEnum(Register.RPC)] + poff9(i);
132 uf(dr(i));
133 },
134 inline .not => {
135 registers[dr(i)] = ~ registers[sr1(i)];
136 uf(dr(i));
137 },
138 inline .st => {
139 mw(registers[@intFromEnum(Register.RPC)] + poff9(i), registers[dr(i)]);
140 },
141 inline .sti => {
142 mw(mr(registers[@intFromEnum(Register.RPC)] + poff9(i)), registers[dr(i)]);
143 },
144 inline .str => {
145 mw(registers[sr1(i)] + poff(i), registers[dr(i)]);
146 },
147 inline .jmp => {
148 registers[@intFromEnum(Register.RPC)] = registers[br(i)];
149 },
150 inline .jsr => {
151 registers[@intFromEnum(Register.R7)] = registers[@intFromEnum(Register.RPC)];
152 registers[@intFromEnum(Register.RPC)] = if (fl(i) == 1) (registers[@intFromEnum(Register.RPC)] + poff11(i)) else registers[br(i)];
153 },
154 inline .br => {
155 if (registers[@intFromEnum(Register.RCND)] & fcnd(i) == 1) {
156 registers[@intFromEnum(Register.RPC)] += poff9(i);
157 }
158 },
159 inline .unused, .rti => {
160 },
161 inline .trap => {
162 switch (TrapFunc.parse(trp(i) - 0x20)) {
163 inline .tgetc => {
164 const dest: []u8 = try allocator.alloc(u8, 1);
165 _ = try std.fs.File.stdin().read(dest);
166 registers[@intFromEnum(Register.R0)] = dest[0];
167 },
168 inline .tout => {
169 std.debug.print("{}", .{ registers[@intFromEnum(Register.R0)] });
170 },
171 inline .tputs => {
172 },
173 inline .tin => {
174 const buffer: []u8 = try allocator.alloc(u8, 1024);
175 defer allocator.free(buffer);
176
177 var stdin = std.fs.File.stdin().reader(buffer).interface;
178 const m = try stdin.takeInt(u8, .little);
179 registers[@intFromEnum(Register.R0)] = m;
180 },
181 inline .thalt => {
182 running = false;
183 },
184 inline .tinu16 => {
185 const buffer: []u8 = try allocator.alloc(u8, 1024);
186 defer allocator.free(buffer);
187
188 var line_buffer: []u8 = try allocator.alloc(u8, 1024);
189 defer allocator.free(line_buffer);
190 var writer: std.io.Writer = .fixed(line_buffer);
191
192 var stdin = std.fs.File.stdin().reader(buffer).interface;
193
194 const line_length = try stdin.streamDelimiterLimit(&writer, '\n', .unlimited);
195 const v: u16 = try std.fmt.parseInt(u16, line_buffer[0..line_length], 10);
196 registers[@intFromEnum(Register.R0)] = v;
197 },
198 inline .tputsp => {
199 },
200 inline .toutu16 => {
201 const buffer: []u8 = try allocator.alloc(u8, 1024);
202 defer allocator.free(buffer);
203
204 const v: u16 = registers[@intFromEnum(Register.R0)];
205 std.debug.print("{}\n", .{ v });
206 },
207 }
208 }
209 }
210}
211
212pub fn start(allocator: std.mem.Allocator, offset: u16) !void {
213 registers[@intFromEnum(Register.RPC)] = pcStart + offset;
214 while (running) {
215 const instruction = mr(registers[@intFromEnum(Register.RPC)]);
216 registers[@intFromEnum(Register.RPC)] += 1;
217
218 try evaluate(allocator, instruction);
219 }
220}
221