commit 4a08b87af4d96853624635955d73a0a53ef20866 Author: rus07tam Date: Mon Nov 3 12:43:30 2025 +0000 initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7220095 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.zig-cache +.direnv +result +zig-out \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c577b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..63c1682 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Owa + +A small experimental virtual machine and interpreter for an interpreted language, written in Zig. + +Owa provides a minimal bytecode format, a stack-based VM, and an object model with prototype inheritance and "magic" hooks. It is designed as a playground for building a compact interpreter, adding opcodes, and experimenting with native/built-in functions. + +## Features + +- **Stack-based VM**: executes a custom bytecode (`OwaInstruction`). +- **Interpreter/threads**: `OwaInterpreter` manages `OwaThread` instances that run frames and bytecode. +- **Frames and locals**: each call executes in an `OwaFrame` with its own instruction pointer and locals map. +- **Object model**: `OwaObject` supports attributes, prototype chain, and a callable function slot. +- **Magic hooks**: predefined magic names (`__call`, `__str`, etc.) enable dynamic behavior. +- **Builtins module**: includes basic objects such as `OWA_NULL` and a native `print` function. + +## Getting Started + +### Prerequisites + +- Zig (0.12+ recommended) + +### Build and Run + +```bash +zig build +zig build run +``` + +This builds the `Owa` executable and runs the demo program from `src/main.zig`. + +## How It Works + +### Bytecode and Instructions + +Instructions are defined in `src/vm/instruction.zig` as a tagged union `OwaInstruction` with an underlying `enum(u8)` opcode. Examples include `Nop`, `LoadConst`, `Call`, `Return`, and simple stack/rotation ops. Programs are encoded to bytes and then decoded during execution. + +```zig +const encoded = try OwaInstruction.encode_program(&.{ + OwaInstruction{ .LoadConst = 0 }, // push builtins.print + OwaInstruction{ .LoadConst = 1 }, // push OWA_NULL + OwaInstruction{ .Call = 1 }, // call print with 1 arg + OwaInstruction{ .Return = {} }, +}, allocator); +``` + +### Interpreter and Threads + +- `OwaInterpreter` initializes memory, holds thread and module lists, and can `spawn()` new `OwaThread`s. +- `OwaThread` manages a value stack and a current `OwaFrame`; it repeatedly decodes bytecode and dispatches instructions. +- `OwaFrame` tracks the function being executed, the instruction pointer (`ip`), a link to the previous frame, and a locals map. + +### Object Model + +`OwaObject` holds: + +- `attrs`: attribute map (`StringHashMapUnmanaged`) +- `prototype`: optional prototype link +- `call_fn`: optional function (either bytecode-backed or native) + +The VM resolves attribute access via `getAttr` and prototype chaining. Callability is resolved through `call_fn` or the `__call` magic attribute. + +### Builtins + +The `modules/builtins` module defines core objects and natives: + +- `OWA_NULL`: the null value +- `print`: a native function printing its arguments + +## Example + +`src/main.zig` demonstrates creating an interpreter and running a tiny program that calls the builtin `print` with `OWA_NULL`: + +```zig +const std = @import("std"); +const owa = @import("Owa"); +const modules = @import("modules"); + +pub fn main() !void { + const allocator = std.heap.page_allocator; + var interpreter = try owa.OwaInterpreter.init(allocator); + var thread1 = try interpreter.spawn(); + + var func = owa.OwaFunction{ + .owa = .{ + .code = try owa.OwaInstruction.encode_program(&.{ + owa.OwaInstruction{ .LoadConst = 0 }, // builtins.print + owa.OwaInstruction{ .LoadConst = 1 }, // OWA_NULL + owa.OwaInstruction{ .Call = 1 }, + owa.OwaInstruction{ .Return = {} }, + }, allocator), + .var_const = &.{ + &modules.builtins.print, + &modules.builtins.OWA_NULL, + }, + }, + }; + + try thread1.execute(&func); + interpreter.deinit(); +} +``` + +## Project Structure + +- `src/mod.zig`: re-exports core VM types (instructions, objects, interpreter, etc.). +- `src/main.zig`: demo entry point. +- `src/vm/instruction.zig`: opcodes and encode/decode utilities. +- `src/core/object.zig`: object model and attribute logic. +- `src/core/function.zig`: function union (bytecode vs native) and flags. +- `src/core/frame.zig`: call frame and locals. +- `src/core/thread.zig`: thread state, dispatch loop, and call mechanics. +- `src/core/interpreter.zig`: interpreter lifecycle and thread management. +- `src/core/magic.zig`: magic names like `__call`, `__str`, etc. +- `src/modules/builtins/`: built-in objects and native functions. +- `build.zig`: Zig build configuration (modules, executable, run step). + +## Roadmap + +- More opcodes (arithmetic, control flow, comparisons, etc.). +- Exception handling and error objects. +- Strings, numbers, and collections as first-class runtime objects. +- Module system and loader. +- Basic standard library. + +## License + +See `LICENSE` for details. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..5e178d4 --- /dev/null +++ b/build.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const owa = b.addModule("Owa", .{ + .root_source_file = b.path("src/mod.zig"), + .target = target, + }); + const modules = b.addModule("modules", .{ + .root_source_file = b.path("src/modules/mod.zig"), + .target = target, + .imports = &.{ + .{ .name = "Owa", .module = owa }, + }, + }); + + const exe = b.addExecutable(.{ + .name = "Owa", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "Owa", .module = owa }, + .{ .name = "modules", .module = modules }, + }, + }), + .use_llvm = true, + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8c98631 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,81 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .Owa, + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0xde4cad10aa799080, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.15.1", + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9aa7f11 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1760038930, + "narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..580a5b1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + description = "Zig Template"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + nativeBuildInputs = with pkgs; [ + zig + zls + python312 + ]; + + buildInputs = with pkgs; [ + gdb + lldb + nixfmt + ]; + in + { + devShells.default = pkgs.mkShell { inherit nativeBuildInputs buildInputs; }; + + packages.default = pkgs.stdenv.mkDerivation { + pname = "Owa"; + version = "0.0.1"; + src = ./.; + + nativeBuildInputs = nativeBuildInputs ++ [ + pkgs.zig.hook + ]; + inherit buildInputs; + }; + } + ); +} diff --git a/src/core/frame.zig b/src/core/frame.zig new file mode 100644 index 0000000..34811df --- /dev/null +++ b/src/core/frame.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const OwaObject = @import("object.zig").OwaObject; +const OwaFunction = @import("function.zig").OwaFunction; + +pub const OwaFrame = struct { + prev: ?*OwaFrame, + func: *OwaFunction, + ip: usize, + locals: *std.StringHashMap(*OwaObject), + + pub fn init(allocator: std.mem.Allocator, func: *OwaFunction) !*OwaFrame { + const locals = try allocator.create(std.StringHashMap(*OwaObject)); + const frame = try allocator.create(OwaFrame); + frame.prev = null; + frame.ip = 0; + frame.func = func; + frame.locals = locals; + return frame; + } +}; diff --git a/src/core/function.zig b/src/core/function.zig new file mode 100644 index 0000000..23fcb5b --- /dev/null +++ b/src/core/function.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const OwaObject = @import("object.zig").OwaObject; +const OwaThread = @import("thread.zig").OwaThread; + +pub const OwaFunctionNative = *const fn (thread: *OwaThread, args: []const *OwaObject) anyerror!*OwaObject; + +pub const OwaFunctionFlags = packed struct(u16) { + varargs: bool = false, + varkwargs: bool = false, + __reserved3: bool = false, + __reserved4: bool = false, + __reserved5: bool = false, + __reserved6: bool = false, + __reserved7: bool = false, + __reserved8: bool = false, + __reserved9: bool = false, + __reserved10: bool = false, + __reserved11: bool = false, + __reserved12: bool = false, + __reserved13: bool = false, + __reserved14: bool = false, + __reserved15: bool = false, + __reserved16: bool = false, +}; + +pub const OwaFunction = union(enum) { + owa: struct { + flags: OwaFunctionFlags = .{}, + code: []const u8 = "", + n_posonly: u8 = 0, + n_kwonly: u8 = 0, + var_const: []const *OwaObject = &.{}, + var_names: []([]const u8) = &.{}, + }, + native: OwaFunctionNative, + + pub fn init_native(native: OwaFunctionNative) OwaFunction { + return OwaFunction{ .native = native }; + } +}; diff --git a/src/core/interpreter.zig b/src/core/interpreter.zig new file mode 100644 index 0000000..a0b0fe3 --- /dev/null +++ b/src/core/interpreter.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const OwaThread = @import("thread.zig").OwaThread; + +pub const OwaInterpreterState = enum { idle, running, paused }; + +pub const OwaInterpreter = struct { + allocator: std.mem.Allocator, + status: OwaInterpreterState, + threads: *std.ArrayList(OwaThread), + modules: *std.ArrayList(OwaThread), + + pub fn init(allocator: std.mem.Allocator) !OwaInterpreter { + const threads = try allocator.create(std.ArrayList(OwaThread)); + threads.* = std.ArrayList(OwaThread).empty; + + const modules = try allocator.create(std.ArrayList(OwaThread)); + modules.* = std.ArrayList(OwaThread).empty; + + return OwaInterpreter{ + .allocator = allocator, + .status = .idle, + .threads = threads, + .modules = modules, + }; + } + + pub fn deinit(self: *OwaInterpreter) void { + defer self.allocator.destroy(self.modules); + defer self.allocator.destroy(self.threads); + for (self.threads.items) |*thread| { + thread.deinit(); + } + + for (self.modules.items) |*module| { + module.deinit(); + } + } + + pub fn spawn(self: *OwaInterpreter) !*OwaThread { + try self.threads.append(self.allocator, try OwaThread.init(self)); + return &self.threads.items[self.threads.items.len - 1]; + } +}; diff --git a/src/core/magic.zig b/src/core/magic.zig new file mode 100644 index 0000000..fa990d0 --- /dev/null +++ b/src/core/magic.zig @@ -0,0 +1,13 @@ +pub const OwaMagic = enum { + pub const construct = "__construct"; + pub const destruct = "__destruct"; + + pub const attr_get = "___attr_get"; + pub const attr_set = "___attr_set"; + pub const attr_del = "___attr_del"; + + pub const call = "__call"; + pub const cell = "__cell"; + + pub const str = "__str"; +}; diff --git a/src/core/module.zig b/src/core/module.zig new file mode 100644 index 0000000..0926d09 --- /dev/null +++ b/src/core/module.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const OwaObject = @import("object.zig").OwaObject; + +pub fn OwaModule(comptime T: type) type { + return struct { + on_load: ?*const fn () anyerror!T = null, + on_unload: ?*const fn () anyerror!T = null, + }; +} diff --git a/src/core/object.zig b/src/core/object.zig new file mode 100644 index 0000000..4d860fd --- /dev/null +++ b/src/core/object.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const OwaFunction = @import("function.zig").OwaFunction; +const OwaMagic = @import("magic.zig").OwaMagic; +const OwaThread = @import("thread.zig").OwaThread; + +pub const OwaObject = struct { + attrs: std.StringHashMapUnmanaged(*OwaObject) = std.StringHashMapUnmanaged(*OwaObject).empty, + prototype: ?*OwaObject = null, + call_fn: ?OwaFunction = null, + cell: []const u8 = "", + + pub fn init(allocator: std.mem.Allocator) !*OwaObject { + const obj = try allocator.create(OwaObject); + obj.* = OwaObject{}; + return obj; + } + + pub fn getAttr(self: *OwaObject, name: []const u8) ?*OwaObject { + if (self.attrs.get(name)) |val| return val; + if (self.prototype) |proto| return proto.getAttr(name); + return null; + } + + pub fn setAttr(self: *OwaObject, allocator: std.mem.Allocator, name: []const u8, val: *OwaObject) !void { + try self.attrs.put(allocator, name, val); + } + + pub fn delAttr(self: *OwaObject, name: []const u8) void { + _ = self.attrs.remove(name); + } + + pub fn getFunc(self: *OwaObject) ?*OwaFunction { + const call_attr = self.getAttr(OwaMagic.call); + if (call_attr) |attr| return attr.getFunc(); + return &self.call_fn.?; + } +}; diff --git a/src/core/thread.zig b/src/core/thread.zig new file mode 100644 index 0000000..13a2a30 --- /dev/null +++ b/src/core/thread.zig @@ -0,0 +1,231 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const OwaFrame = @import("frame.zig").OwaFrame; +const OwaFunction = @import("function.zig").OwaFunction; +const OwaInterpreter = @import("interpreter.zig").OwaInterpreter; +const OwaInstruction = @import("../vm/instruction.zig").OwaInstruction; +const OwaMagic = @import("magic.zig").OwaMagic; +const OwaObject = @import("object.zig").OwaObject; + +pub const OwaThreadState = union(enum) { + initializing, + running: struct { + frame: *OwaFrame, + }, + finished, + exception: struct { + frame: *OwaFrame, + exception: *OwaObject, + }, +}; + +pub const OwaThread = struct { + interpreter: *OwaInterpreter, + state: OwaThreadState, + stack: *std.ArrayList(*OwaObject), + + pub fn init(interpreter: *OwaInterpreter) !OwaThread { + const stack = try interpreter.allocator.create(std.ArrayList(*OwaObject)); + stack.* = std.ArrayList(*OwaObject).empty; + + return OwaThread{ + .interpreter = interpreter, + .state = .initializing, + .stack = stack, + }; + } + + pub fn deinit(self: *OwaThread) void { + self.stack.deinit(self.interpreter.allocator); + } + + pub fn execute(self: *OwaThread, func: *OwaFunction) !void { + self.state = .initializing; + self.state = .{ + .running = .{ + .frame = try OwaFrame.init(self.interpreter.allocator, func), + }, + }; + while (self.state == .running) { + try self.step(); + } + if (self.state == .initializing) return error.NotValidState; + if (self.state == .exception) return error.NotImplemented; + self.stack.clearRetainingCapacity(); + } + + fn step(self: *OwaThread) !void { + if (self.state != .running) return; + switch (self.state.running.frame.func.*) { + .owa => { + if (self.state.running.frame.ip >= self.state.running.frame.func.owa.code.len) { + self.state = .finished; + } else { + const code = self.state.running.frame.func.owa.code[self.state.running.frame.ip..]; + const parsed = try OwaInstruction.decode(code); + self.state.running.frame.ip += parsed.len; + try self.dispatch(parsed.inst); + } + }, + .native => { + const val = try self.state.running.frame.func.native(self, &.{}); + try self.stack.append(self.interpreter.allocator, val); + self.prev_frame(); + }, + } + } + + fn dispatch(self: *OwaThread, instr: OwaInstruction) !void { + switch (instr) { + .Nop => |_| {}, + .PopTop => |_| { + _ = self.stack.pop(); + }, + .DupTop => |_| { + const x = self.stack.getLast(); + try self.stack.append(self.interpreter.allocator, x); + }, + .RotTwo => |_| { + if (self.stack.items.len < 2) return; + const a = self.stack.pop().?; + const b = self.stack.pop().?; + try self.stack.append(self.interpreter.allocator, a); + try self.stack.append(self.interpreter.allocator, b); + }, + .RotThree => |_| { + if (self.stack.items.len < 3) return; + const a = self.stack.pop().?; + const b = self.stack.pop().?; + const c = self.stack.pop().?; + try self.stack.append(self.interpreter.allocator, a); + try self.stack.append(self.interpreter.allocator, b); + try self.stack.append(self.interpreter.allocator, c); + }, + .RotFour => |_| { + if (self.stack.items.len < 4) return; + const a = self.stack.pop().?; + const b = self.stack.pop().?; + const c = self.stack.pop().?; + const d = self.stack.pop().?; + try self.stack.append(self.interpreter.allocator, a); + try self.stack.append(self.interpreter.allocator, b); + try self.stack.append(self.interpreter.allocator, c); + try self.stack.append(self.interpreter.allocator, d); + }, + .LoadConst => |arg| { + const val = self.state.running.frame.func.owa.var_const[arg]; + try self.stack.append(self.interpreter.allocator, val); + }, + .LoadFast => |arg| { + const name = self.state.running.frame.func.owa.var_names[arg]; + const val = self.state.running.frame.locals.get(name); + try self.stack.append(self.interpreter.allocator, val.?); + }, + .LoadAttr => |name_idx| { + const name = self.state.running.frame.func.owa.var_names[name_idx]; + var obj = self.stack.pop().?; + const val = obj.getAttr(name).?; + try self.stack.append(self.interpreter.allocator, val); + }, + .StoreFast => |arg| { + const name = self.state.running.frame.func.owa.var_names[arg]; + const val = self.stack.pop(); + try self.state.running.frame.locals.put(name, val.?); + }, + .StoreAttr => |name_idx| { + const name = self.state.running.frame.func.owa.var_names[name_idx]; + var obj = self.stack.pop().?; + const val = self.stack.pop().?; + try obj.setAttr(self.interpreter.allocator, name, val); + }, + .DeleteFast => |arg| { + const name = self.state.running.frame.func.owa.var_names[arg]; + _ = self.state.running.frame.locals.remove(name); + }, + .DeleteAttr => |name_idx| { + const name = self.state.running.frame.func.owa.var_names[name_idx]; + var obj = self.stack.pop().?; + obj.delAttr(name); + }, + .Return => |_| { + const val = self.stack.pop(); + try self.stack.append(self.interpreter.allocator, val.?); + self.prev_frame(); + }, + .ReturnConst => |arg| { + const val = self.state.running.frame.func.owa.var_const[arg]; + try self.stack.append(self.interpreter.allocator, val); + self.prev_frame(); + }, + .ReturnFast => |arg| { + const name = self.state.running.frame.func.owa.var_names[arg]; + const val = self.state.running.frame.locals.get(name); + try self.stack.append(self.interpreter.allocator, val.?); + self.prev_frame(); + }, + .ReturnAttr => |name_idx| { + const name = self.state.running.frame.func.owa.var_names[name_idx]; + var obj = self.stack.pop().?; + const val = obj.getAttr(name).?; + try self.stack.append(self.interpreter.allocator, val); + self.prev_frame(); + }, + .Call => |args_count| { + var args: []*OwaObject = try self.interpreter.allocator.alloc(*OwaObject, args_count); + if (args_count >= self.stack.items.len) unreachable; + for (0..args_count) |i| { + args[i] = self.stack.pop().?; + } + const func = self.stack.pop().?; + if (func.call_fn != null) { + _ = try self.new_frame(func); + } + }, + .Jump => |value| { + if (value >= self.state.running.frame.func.owa.code.len) unreachable; + self.state.running.frame.ip = value; + }, + else => { + unreachable; + }, + } + } + + pub fn call(self: *OwaThread, obj: *OwaObject) !*OwaObject { + const prev_depth = self.get_depth(); + _ = try self.new_frame(&obj.call_fn.?); + while (self.state == .running and self.get_depth() != prev_depth) { + try self.step(); + } + if (self.state == .initializing or self.stack.capacity == 0) return error.NotValidState; + if (self.state == .exception) return error.NotImplemented; + return self.stack.pop().?; + } + + fn get_depth(self: @This()) usize { + if (self.state == .finished or self.state == .initializing) return 0; + var depth: usize = 1; + var frame = if (self.state == .exception) self.state.exception.frame else self.state.running.frame; + while (frame.prev != null) { + depth += 1; + frame = frame.prev.?; + } + return depth; + } + + inline fn new_frame(self: *OwaThread, obj: *OwaObject) !*OwaFrame { + const prev = self.state.running.frame; + const func = obj.getFunc().?; + self.state.running.frame = try OwaFrame.init(self.interpreter.allocator, func); + self.state.running.frame.prev = prev; + return self.state.running.frame; + } + + inline fn prev_frame(self: *OwaThread) void { + if (self.state.running.frame.prev != null) { + self.state.running.frame = self.state.running.frame.prev.?; + } else { + self.state = .finished; + } + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ae23811 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const owa = @import("Owa"); +const modules = @import("modules"); + +pub fn main() !void { + const allocator = std.heap.page_allocator; + var interpreter = try owa.OwaInterpreter.init(allocator); + var thread1 = try interpreter.spawn(); + + var func2 = owa.OwaFunction{ + .owa = .{ + .code = try owa.OwaInstruction.encode_program(&.{ + owa.OwaInstruction{ .LoadConst = 0 }, // BUILTIN_PRINT + owa.OwaInstruction{ .LoadConst = 1 }, // OWA_NULL + owa.OwaInstruction{ .Call = 1 }, + owa.OwaInstruction{ .Return = {} }, + }, allocator), + .var_const = &.{ + &modules.builtins.print, + &modules.builtins.OWA_NULL, + }, + }, + }; + + try thread1.execute(&func2); + + interpreter.deinit(); +} diff --git a/src/mod.zig b/src/mod.zig new file mode 100644 index 0000000..beedf72 --- /dev/null +++ b/src/mod.zig @@ -0,0 +1,17 @@ +pub const OwaInstruction = @import("vm/instruction.zig").OwaInstruction; +pub const OwaOpcode = @import("vm/instruction.zig").OwaOpcode; + +pub const OwaFrame = @import("core/frame.zig").OwaFrame; + +pub const OwaFunction = @import("core/function.zig").OwaFunction; +pub const OwaFunctionNative = @import("core/function.zig").OwaFunctionNative; +pub const OwaFunctionFlags = @import("core/function.zig").OwaFunctionFlags; + +pub const OwaInterpreter = @import("core/interpreter.zig").OwaInterpreter; +pub const OwaInterpreterState = @import("core/interpreter.zig").OwaInterpreterState; + +pub const OwaMagic = @import("core/magic.zig").OwaMagic; +pub const OwaModule = @import("core/module.zig").OwaModule; +pub const OwaObject = @import("core/object.zig").OwaObject; +pub const OwaThread = @import("core/thread.zig").OwaThread; +pub const OwaThreadState = @import("core/thread.zig").OwaThreadState; diff --git a/src/modules/builtins/mod.zig b/src/modules/builtins/mod.zig new file mode 100644 index 0000000..20457cc --- /dev/null +++ b/src/modules/builtins/mod.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const owa = @import("Owa"); + +pub var OWA_TYPE = owa.OwaObject{}; + +pub var OWA_OBJECT = owa.OwaObject{ + .prototype = &OWA_TYPE, +}; + +pub var OWA_FUNCTION = owa.OwaObject{ + .prototype = &OWA_TYPE, +}; + +pub var OWA_NULL = owa.OwaObject{ + .prototype = &OWA_TYPE, +}; + +pub var OWA_STR = owa.OwaObject{ + .prototype = &OWA_TYPE, +}; + +pub fn createFunction(allocator: owa.OwaAllocator, func: owa.OwaFunctionNative) !*owa.OwaObject { + const obj = try owa.OwaObject.init(allocator); + obj.call_fn = .{ .native = func }; + obj.prototype = &OWA_FUNCTION; + return obj; +} + +fn _owaBuiltinPrint(_: *owa.OwaThread, args: []const *owa.OwaObject) !*owa.OwaObject { + std.debug.print("{any}\n", .{args}); + return &OWA_NULL; +} + +pub var print = owa.OwaObject{ + .prototype = &OWA_FUNCTION, + .call_fn = .{ .native = _owaBuiltinPrint }, +}; diff --git a/src/modules/mod.zig b/src/modules/mod.zig new file mode 100644 index 0000000..c7cc32e --- /dev/null +++ b/src/modules/mod.zig @@ -0,0 +1 @@ +pub const builtins = @import("builtins/mod.zig"); diff --git a/src/vm/instruction.zig b/src/vm/instruction.zig new file mode 100644 index 0000000..16b38dd --- /dev/null +++ b/src/vm/instruction.zig @@ -0,0 +1,100 @@ +const std = @import("std"); + +pub const OwaOpcode = @typeInfo(OwaInstruction).@"union".tag_type.?; + +pub const OWA_INSTRUCTION_COUNT = @typeInfo(OwaInstruction).@"union".fields.len; + +pub const OwaInstruction = union(enum(u8)) { + Nop: void = 0x00, + PopTop: void = 0x01, + DupTop: void = 0x02, + RotTwo: void = 0x03, + RotThree: void = 0x04, + RotFour: void = 0x05, + LoadConst: usize = 0x06, + LoadFast: usize = 0x07, + LoadAttr: usize = 0x08, + LoadItem: usize = 0x09, + StoreFast: usize = 0x0A, + StoreAttr: usize = 0x0B, + StoreItem: usize = 0x0C, + DeleteFast: usize = 0x0D, + DeleteAttr: usize = 0x0E, + DeleteItem: usize = 0x0F, + Return: void = 0x10, + ReturnConst: usize = 0x11, + ReturnFast: usize = 0x12, + ReturnAttr: usize = 0x13, + Call: usize = 0x14, + Jump: usize = 0x15, + Unknown: void = 0xFF, + + pub fn encode(self: OwaInstruction, allocator: std.mem.Allocator) ![]const u8 { + inline for (@typeInfo(OwaInstruction).@"union".fields, @typeInfo(OwaOpcode).@"enum".fields) |field, e_field| { + if (std.mem.eql(u8, field.name, @tagName(self))) { + const size: comptime_int = @sizeOf(field.type); + const total_size: comptime_int = size + 1; + const args = try allocator.alloc(u8, total_size); + args[0] = e_field.value; + + switch (field.type) { + void => {}, + usize => { + std.mem.writeInt(usize, args[1..total_size], @field(self, field.name), .big); + }, + else => { + std.debug.panic("Not implemented type: {any} of .{s}\n", .{ field.type, field.name }); + unreachable; + }, + } + + return args; + } + } + var fallback = try allocator.alloc(u8, 1); + fallback[0] = 0x00; + return fallback; + } + + pub fn decode(raw: []const u8) !struct { inst: OwaInstruction, len: usize } { + if (raw.len < 1) return error.InstructionTooShort; + + const opcode = raw[0]; + var ret_size: usize = 1; + var inst: OwaInstruction = OwaInstruction{ .Nop = {} }; + + inline for (@typeInfo(OwaOpcode).@"enum".fields, @typeInfo(OwaInstruction).@"union".fields) |o, field| { + if (o.value == opcode) { + const size = @sizeOf(field.type); + ret_size = size; + + if (raw.len < size + 1) return error.InvalidArgumentCount; + const args: *const [size]u8 = raw[1 .. size + 1]; + + switch (field.type) { + void => { + inst = @unionInit(OwaInstruction, field.name, {}); + }, + usize => { + const val = std.mem.readInt(usize, args, .big); + inst = @unionInit(OwaInstruction, field.name, val); + }, + else => { + std.debug.panic("Not implemented type: {any} of .{s}\n", .{ field.type, field.name }); + unreachable; + }, + } + } + } + return .{ .inst = inst, .len = ret_size + 1 }; + } + + pub fn encode_program(program: []const OwaInstruction, allocator: std.mem.Allocator) ![]const u8 { + var buffer = std.array_list.Managed(u8).init(allocator); + for (program) |instr| { + const encoded = try instr.encode(allocator); + try buffer.appendSlice(encoded); + } + return buffer.toOwnedSlice(); + } +};