Compare commits

...

7 Commits

Author SHA1 Message Date
fc303ba553 fix build errors 2025-04-02 23:10:32 +09:00
ee8d0cde34 improve doc comments 2025-04-02 22:52:13 +09:00
845582a14d add new container type, Store 2025-04-02 22:30:57 +09:00
907d71a19a add new data type, Node 2025-04-02 22:30:39 +09:00
b9153e2708 fix an issue in findSegment 2025-03-30 08:56:14 +09:00
a1ede8d1a0 refactor Digraph 2025-03-30 08:28:36 +09:00
0bfa0f1098 creates new project 2025-03-30 02:22:02 +09:00
9 changed files with 738 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
/.zig-cache/
/zig-out/

71
build.zig Normal file
View File

@@ -0,0 +1,71 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ---- deps
const dvui_dep = b.dependency("dvui", .{
.target = target,
.optimize = optimize,
.backend = .sdl,
.sdl3 = true,
});
// ---- library
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/hncore/root.zig"),
.target = target,
.optimize = optimize,
});
const lib = b.addLibrary(.{
.linkage = .static,
.name = "hncore",
.root_module = lib_mod,
});
b.installArtifact(lib);
// ---- executable
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/hnet/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("hncore", lib_mod);
exe_mod.addImport("dvui", dvui_dep.module("dvui_sdl"));
const exe = b.addExecutable(.{
.name = "hnet",
.root_module = exe_mod,
});
b.installArtifact(exe);
// ---- running
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// ---- testing
const lib_unit_tests = b.addTest(.{
.root_module = lib_mod,
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
// ---- document generation
const install_docs = b.addInstallDirectory(.{
.source_dir = lib.getEmittedDocs(),
.install_dir = .prefix,
.install_subdir = "docs",
});
const docs_step = b.step("docs", "Install docs into zig-out/docs");
docs_step.dependOn(&install_docs.step);
}

19
build.zig.zon Normal file
View File

@@ -0,0 +1,19 @@
.{
.name = .heavens_net,
.version = "0.0.1",
.fingerprint = 0x5cbe403baf740fcb,
.minimum_zig_version = "0.15.0-dev.149+2b57f6b71",
.dependencies = .{
.dvui = .{
.url = "https://github.com/david-vanderson/dvui/archive/9f446c8600b3385418c8926481076c335261f222.zip",
.hash = "dvui-0.2.0-AQFJmesqywBKCM6r9orR324EGhZvGsjhJw0ZHWcvwzyh",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

295
src/hncore/Digraph.zig Normal file
View File

@@ -0,0 +1,295 @@
const std = @import("std");
/// A data type to store connections of nodes in directional-graph.
pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
return struct {
///
pub const Node = T;
/// A connection between 2 nodes.
/// e.g.) `Conn { .from = X, .to = Y }` => "X is connected to Y"
pub const Conn = struct { from: T, to: T, };
///
pub const ConnList = std.ArrayList(Conn);
///
pub const Error = error {
AlreadyConnected,
NotConnected,
};
///
map: ConnList,
///
pub fn init(alloc: std.mem.Allocator, mapUnsorted: []const Conn) !@This() {
var mapSorted = ConnList.init(alloc);
try mapSorted.ensureTotalCapacity(mapUnsorted.len);
for (mapUnsorted) |conn| {
try mapSorted.append(conn);
}
std.mem.sort(Conn, mapSorted.items, {}, compareConn);
return .{
.map = mapSorted,
};
}
///
pub fn deinit(self: *@This()) void {
self.map.deinit();
}
/// Makes new connection between `from` to `to`.
/// Returns false if they are already connected otherwise true.
pub fn connectIf(self: *@This(), from: T, to: T) !bool {
const begin, const end = self.findSegment(from);
if (self.findConnectionInSegment(from, to, begin, end)) |_| {
return false;
} else {
try self.map.insert(end, Conn { .from = from, .to = to, });
return true;
}
}
/// Same to `connectIf`, but returns an error if it's already connected.
pub fn connect(self: *@This(), from: T, to: T) !void {
if (!try self.connectIf(from, to)) {
return Error.AlreadyConnected;
}
}
/// Removes an existing connection between `from` to `to`.
/// Returns false if they are not connected otherwise true.
pub fn disconnectIf(self: *@This(), from: T, to: T) bool {
const begin, const end = self.findSegment(from);
if (self.findConnectionInSegment(from, to, begin, end)) |idx| {
_ = self.map.orderedRemove(idx);
return true;
} else {
return false;
}
}
/// Same to `disconnectIf`, but returns an error if it's not connected.
pub fn disconnect(self: *@This(), from: T, to: T) !void {
if (!self.disconnectIf(from, to)) {
return Error.NotConnected;
}
}
///
pub fn isConnected(self: *const @This(), from: T, to: T) bool {
const begin, const end = self.findSegment(from);
return self.findConnectionInSegment(from, to, begin, end) != null;
}
///
pub fn getChildrenOf(self: *const @This(), from: T) []const Conn {
const begin, const end = self.findSegment(from);
return self.map.items[begin..end];
}
fn findConnectionInSegment(self: *const @This(), from: T, to: T, begin: usize, end: usize) ?usize {
for (self.map.items[begin..end], begin..) |v, idx| {
if ((v.from == from) and (v.to == to)) {
return idx;
}
}
return null;
}
fn findSegment(self: *const @This(), from: T) struct { usize, usize } {
const n = self.map.items.len;
if (n == 0) {
return .{ 0, 0, };
}
const baseIdx = self.binsearch(from).?;
const baseFrom = self.map.items[baseIdx].from;
var begin: usize = undefined;
var end : usize = undefined;
if (baseFrom < from) {
begin = baseIdx;
while ((begin < n) and (self.map.items[begin].from < from)) { begin += 1; }
end = begin;
while ((end < n) and (self.map.items[end].from == from)) { end += 1; }
} else if (baseFrom > from) {
end = baseIdx;
while ((end > 0) and (self.map.items[end-1].from > from)) { end -= 1; }
begin = end;
while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; }
} else {
begin = baseIdx;
while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; }
end = baseIdx;
while ((end < n) and (self.map.items[end].from == from)) { end += 1; }
}
return .{ begin, end, };
}
fn binsearch(self: *const @This(), from: T) ?usize {
if (self.map.items.len == 0) {
return null;
}
var left : usize = 0;
var right: usize = self.map.items.len;
var idx: usize = undefined;
while (left < right) {
idx = (left + right) / 2;
const target = self.map.items[idx].from;
if (target < from) {
left = idx + 1;
} else if (target > from) {
right = idx -| 1;
} else {
break;
}
}
return idx;
}
fn compareConn(_: void, a: Conn, b: Conn) bool {
return lessThanFn(a.from, b.from);
}
};
}
/// A type of comparator function for the type T, which is to be passed as an argument of `Digraph()`.
pub fn LessThanFunc(comptime T: type) type {
return fn (lhs: T, rhs: T) bool;
}
/// Returns a lessThanFunc for the comparable type T.
pub fn lessThanFuncFor(comptime T: type) LessThanFunc(T) {
return struct {
fn inner(lhs: T, rhs: T) bool {
if (@typeInfo(T) == .pointer) {
return @intFromPtr(lhs) < @intFromPtr(rhs);
} else {
return lhs < rhs;
}
}
}.inner;
}
test "compile check for various types" {
_ = Digraph(u8, lessThanFuncFor(u8));
_ = Digraph(u16, lessThanFuncFor(u16));
_ = Digraph(i8, lessThanFuncFor(i8));
_ = Digraph(i16, lessThanFuncFor(i16));
_ = Digraph(*i8, lessThanFuncFor(*i8));
_ = Digraph(*anyopaque, lessThanFuncFor(*anyopaque));
}
test "check if connected" {
const Sut = Digraph(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn {
.{ .from = 3, .to = 0, },
.{ .from = 0, .to = 1, },
.{ .from = 1, .to = 3, },
};
var sut = try Sut.init(std.testing.allocator, map[0..]);
defer sut.deinit();
try std.testing.expect(sut.isConnected(0, 1));
try std.testing.expect(!sut.isConnected(1, 0));
try std.testing.expect(sut.isConnected(1, 3));
try std.testing.expect(!sut.isConnected(3, 1));
try std.testing.expect(sut.isConnected(3, 0));
try std.testing.expect(!sut.isConnected(0, 3));
try std.testing.expect(!sut.isConnected(0, 2));
try std.testing.expect(!sut.isConnected(2, 0));
try std.testing.expect(!sut.isConnected(1, 2));
try std.testing.expect(!sut.isConnected(2, 1));
}
test "make new connection" {
const Sut = Digraph(u8, lessThanFuncFor(u8));
var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit();
try std.testing.expect(try sut.connectIf(2, 1));
try std.testing.expect(sut.isConnected(2, 1));
try std.testing.expect(!sut.isConnected(1, 2));
try sut.connect(3, 1);
try std.testing.expect(sut.isConnected(3, 1));
try std.testing.expect(!sut.isConnected(1, 3));
}
test "making an existing connection fails" {
const Sut = Digraph(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn {
.{ .from = 0, .to = 1, },
};
var sut = try Sut.init(std.testing.allocator, map[0..]);
defer sut.deinit();
try std.testing.expect(!try sut.connectIf(0, 1));
try std.testing.expectError(Sut.Error.AlreadyConnected, sut.connect(0, 1));
}
test "disconnect an existing connection" {
const Sut = Digraph(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn {
.{ .from = 0, .to = 1, },
.{ .from = 2, .to = 3, },
};
var sut = try Sut.init(std.testing.allocator, map[0..]);
defer sut.deinit();
try std.testing.expect(sut.disconnectIf(0, 1));
try std.testing.expect(!sut.isConnected(0, 1));
try sut.disconnect(2, 3);
try std.testing.expect(!sut.isConnected(2, 3));
}
test "disconnecting a missing connection fails" {
const Sut = Digraph(u8, lessThanFuncFor(u8));
var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit();
try std.testing.expect(!sut.disconnectIf(0, 1));
try std.testing.expectError(Sut.Error.NotConnected, sut.disconnect(1, 0));
}
test "chaotic operation" {
const Sut = Digraph(u16, lessThanFuncFor(u16));
var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit();
const N = 100;
const P = 109;
const Q = 113;
const R = 127;
for (0..N) |v| {
const x: Sut.Node = @intCast(v);
try sut.connect((x*Q)%P, (x*R)%P);
}
for (N/2..N) |v| {
const x: Sut.Node = @intCast(v);
try sut.disconnect((x*Q)%P, (x*R)%P);
}
for (0..N/2) |v| {
const x: Sut.Node = @intCast(v);
try std.testing.expect(sut.isConnected((x*Q)%P, (x*R)%P));
}
for (N/2..N) |v| {
const x: Sut.Node = @intCast(v);
try std.testing.expect(!sut.isConnected((x*Q)%P, (x*R)%P));
}
}

37
src/hncore/Node.zig Normal file
View File

@@ -0,0 +1,37 @@
const std = @import("std");
///
pub const Node = struct {
/// unique and immutable integer
id: usize,
/// summary text of this node
summary: []const u8,
///
pub fn init(alloc: std.mem.Allocator, id: usize, summary: []const u8) !Node {
return .{
.id = id,
.summary = try alloc.dupe(u8, summary),
};
}
/// pass the same allocator as init() call
pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void {
alloc.free(self.summary);
}
};
test "serialize" {
const alloc = std.testing.allocator;
var node = try Node.init(alloc, 0, "helloworld");
defer node.deinit(alloc);
var json = std.ArrayList(u8).init(alloc);
defer json.deinit();
try std.json.stringify(node, .{}, json.writer());
try std.testing.expectEqualStrings(
json.items,
\\{"id":0,"summary":"helloworld"}
);
}

210
src/hncore/Store.zig Normal file
View File

@@ -0,0 +1,210 @@
const std = @import("std");
/// A container for all existing instances of T.
/// This is like a dedicated allocator for the type, T, with a refcount system.
pub fn Store(comptime T: type) type {
return struct {
///
pub const Item = T;
///
pub const VTable = struct {
///
udata: ?*anyopaque = null,
///
deinitItem: ?*fn (?*anyopaque, *T) void = null,
};
/// A struct which holds an item and its reference count.
pub const Slot = struct {
item : Item,
refcnt: usize,
pub fn ref(self: *@This()) void {
self.refcnt += 1;
}
pub fn unref(self: *@This()) void {
self.refcnt -= 1;
}
};
///
pub const Error = error {
DetectedItemLeak,
};
///
alloc : std.mem.Allocator,
///
vtable: VTable,
///
slots : std.ArrayList(*Slot),
///
pub fn init(alloc: std.mem.Allocator, vtable: VTable) @This() {
return .{
.alloc = alloc,
.vtable = vtable,
.slots = std.ArrayList(*Slot).init(alloc),
};
}
///
pub fn deinit(self: *@This()) !void {
defer self.slots.deinit();
try self.collectGarbage();
if (self.slots.items.len > 0) {
return Error.DetectedItemLeak;
}
}
/// Returns a slot which contains newly-allocated item.
/// - A reference count of the returned slot is 1
/// - The returned slot and its contents are alive till the reference count becomes 0
pub fn add(self: *@This(), item: Item) !*Slot {
for (self.slots.items) |slot| {
if (slot.refcnt == 0) {
self.deinitItem(&slot.item);
slot.* = .{ .item = item, .refcnt = 1, };
return slot;
}
}
const newSlot = try self.alloc.create(Slot);
errdefer self.alloc.destroy(newSlot);
newSlot.* = .{ .item = item, .refcnt = 1, };
try self.slots.append(newSlot);
return newSlot;
}
/// Releases memory of slots whose reference count is 0.
/// Usually no need to call this function because such slots will be reused.
pub fn collectGarbage(self: *@This()) !void {
var slots = self.slots.items;
var set: usize = 0;
var get: usize = 0;
while (set < slots.len) {
if (slots[set].refcnt == 0) {
self.deinitItem(&slots[set].item);
self.alloc.destroy(slots[set]);
get += 1;
if (get >= slots.len) {
break;
}
slots[set] = slots[get];
} else {
set += 1;
get += 1;
}
}
try self.slots.resize(set);
}
fn deinitItem(self: *@This(), item: *Item) void {
if (self.vtable.deinitItem != null) {
self.vtable.deinitItem.?(self.vtable.udata, item);
}
}
};
}
test "add new item" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
{
var slot = try sut.add(123);
defer slot.unref();
// check initial value
try std.testing.expect(slot.refcnt == 1);
try std.testing.expect(slot.item == 123);
// check refcnt handling
slot.ref();
try std.testing.expect(slot.refcnt == 2);
slot.unref();
try std.testing.expect(slot.refcnt == 1);
}
}
test "reuse an expired slot by adding right after removal" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var address: usize = undefined;
{
var slot = try sut.add(123);
defer slot.unref();
address = @intFromPtr(slot);
try std.testing.expect(slot.item == 123);
}
{
var slot = try sut.add(456);
defer slot.unref();
try std.testing.expect(address == @intFromPtr(slot));
try std.testing.expect(slot.item == 456);
}
}
test "reusing doesn't happen by adding twice" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var slot1 = try sut.add(123);
defer slot1.unref();
var slot2 = try sut.add(456);
defer slot2.unref();
try std.testing.expect(slot1 != slot2);
try std.testing.expect(slot1.item == 123);
try std.testing.expect(slot2.item == 456);
}
test "chaotic addition and removals" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var slots = std.ArrayList(*Store(Item).Slot).init(std.testing.allocator);
defer {
for (slots.items) |slot| { slot.unref(); }
slots.deinit();
}
// add 0~300
for (0..300) |i| {
const x: u16 = @intCast(i);
try slots.append(try sut.add(x));
}
// removes other than multiples of 3
for (0..100) |i| {
slots.orderedRemove(300 - (i+1)*3 + 2).unref();
slots.orderedRemove(300 - (i+1)*3 + 1).unref();
}
// add multiples of 3 from 300 to 600
for (0..100) |i| {
const x: u16 = @intCast(i);
try slots.append(try sut.add(300 + x*3));
}
// check if the list is composed of multiples of 3 from 0~600
for (0..200) |i| {
const x: u16 = @intCast(i);
try std.testing.expectEqual(slots.items[i].item, x*3);
}
}

7
src/hncore/root.zig Normal file
View File

@@ -0,0 +1,7 @@
pub const Digraph = @import("./Digraph.zig").Digraph;
pub const Node = @import("./Node.zig").Node;
pub const Store = @import("./Store.zig").Store;
test {
@import("std").testing.refAllDecls(@This());
}

47
src/hnet/App.zig Normal file
View File

@@ -0,0 +1,47 @@
const std = @import("std");
const dvui = @import("dvui");
const hncore = @import("hncore");
pub const App = struct {
pub fn init(alloc: std.mem.Allocator) !App {
_ = alloc;
return App {
};
}
pub fn deinit(self: *App) void {
_ = self;
}
pub fn gui(self: *App) !void {
_ = self;
try gui_menu();
try gui_main();
}
fn gui_menu() !void {
var root = try dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
defer root.deinit();
if (try dvui.menuItemLabel(@src(), "File", .{ .submenu = true }, .{})) |r| {
var float = try dvui.floatingMenu(
@src(), .{.from = dvui.Rect.fromPoint(dvui.Point{ .x = r.x, .y = r.y + r.h })}, .{});
defer float.deinit();
if (try dvui.menuItemLabel(@src(), "Open", .{}, .{})) |_| {
}
}
}
fn gui_main() !void {
var box = try dvui.scrollArea(@src(), .{}, .{
.expand = .both,
.color_fill = .{ .name = .fill_window },
.padding = dvui.Rect.all(8),
});
defer box.deinit();
if (try dvui.button(@src(), "Zoom In", .{}, .{})) {
}
if (try dvui.button(@src(), "Zoom In", .{}, .{})) {
}
}
};

48
src/hnet/main.zig Normal file
View File

@@ -0,0 +1,48 @@
const std = @import("std");
const dvui = @import("dvui");
const hncore = @import("hncore");
const App = @import("./App.zig").App;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) {
@panic("memory leak detected");
};
var backend = try dvui.backend.initWindow(.{
.allocator = gpa.allocator(),
.size = .{ .w = 800, .h = 600, },
.min_size = .{ .w = 250, .h = 350, },
.vsync = true,
.title = "Heaven's Net",
});
defer backend.deinit();
var win = try dvui.Window.init(@src(), gpa.allocator(), backend.backend(), .{});
defer win.deinit();
var app = try App.init(gpa.allocator());
while (true) {
try win.begin(
win.beginWait(backend.hasEvent()),
);
const quit = try backend.addAllEvents(&win);
if (quit) {
break;
}
try app.gui();
const end_micros = try win.end(.{});
backend.setCursor(win.cursorRequested());
backend.textInputRect(win.textInputRequested());
backend.renderPresent();
backend.waitEventTimeout(
win.waitTime(end_micros, null),
);
}
}