diff --git a/build.zig b/build.zig index b453d08..be7e234 100644 --- a/build.zig +++ b/build.zig @@ -1,8 +1,7 @@ const std = @import("std"); -pub fn build(b: *std.Build) void { +pub fn build(b: *std.Build) !void { const use_llvm = true; - const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const mod = b.addModule("ink", .{ @@ -21,11 +20,71 @@ pub fn build(b: *std.Build) void { }), // Added because of a switch on a corrupt value on git hash // a8a6ce8d0f14c1dd5b3ea42e46f3226bcbe11a87 - .use_llvm = true, + .use_llvm = use_llvm, + .use_lld = use_llvm, + }); + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "ink", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/c_api.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "ink", .module = mod }, + }, + }), + .use_llvm = use_llvm, + .use_lld = use_llvm, }); + const pc: *std.Build.Step.InstallFile = pc: { + const file = b.addWriteFile("libink.pc", b.fmt( + \\prefix={s} + \\includedir=${{prefix}}/include + \\libdir=${{prefix}}/lib + \\ + \\Name: libink + \\URL: https://codeberg.org/haxolotl/ink + \\Description: TODO + \\Version: 0.1.0 + \\Cflags: -I${{includedir}} + \\Libs: -L${{libdir}} -link + , .{b.install_prefix})); + break :pc b.addInstallFileWithDir( + file.getDirectory().path(b, "libink.pc"), + .prefix, + "share/pkgconfig/libink.pc", + ); + }; + + const c_header = b.addInstallFileWithDir( + b.path("include/ink.h"), + .header, + "ink/ink.h", + ); + + const examples = try buildExamples(b, target, optimize, lib); + + b.getInstallStep().dependOn(&lib.step); + b.getInstallStep().dependOn(&c_header.step); + b.getInstallStep().dependOn(&pc.step); + b.installArtifact(lib); b.installArtifact(exe); + for (examples) |example_exe| { + b.getInstallStep().dependOn(&b.addInstallArtifact( + example_exe, + .{ + .dest_dir = .{ + .override = .{ + .custom = "bin/example", + }, + }, + }, + ).step); + } + const run_step = b.step("run", "Run the app"); const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); @@ -42,16 +101,8 @@ pub fn build(b: *std.Build) void { }); const run_mod_tests = b.addRunArtifact(mod_tests); - const exe_tests = b.addTest(.{ - .root_module = exe.root_module, - .use_llvm = use_llvm, - .use_lld = use_llvm, - }); - const run_exe_tests = b.addRunArtifact(exe_tests); - const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_mod_tests.step); - test_step.dependOn(&run_exe_tests.step); const run_cover = b.addSystemCommand(&.{ "kcov", @@ -63,3 +114,72 @@ pub fn build(b: *std.Build) void { const cover_step = b.step("cover", "Generate test coverage report"); cover_step.dependOn(&run_cover.step); } + +fn buildExamples( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + c_lib_: ?*std.Build.Step.Compile, +) ![]const *std.Build.Step.Compile { + const alloc = b.allocator; + var steps: std.ArrayList(*std.Build.Step.Compile) = .empty; + defer steps.deinit(alloc); + + var dir = try std.fs.cwd().openDir(try b.build_root.join( + b.allocator, + &.{"examples"}, + ), .{ .iterate = true }); + defer dir.close(); + + var it = dir.iterate(); + while (try it.next()) |entry| { + const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue; + if (index == 0) continue; + + const name = entry.name[0..index]; + const is_zig = std.mem.eql(u8, entry.name[index + 1 ..], "zig"); + const exe: *std.Build.Step.Compile = if (is_zig) exe: { + const exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .root_source_file = b.path(b.fmt( + "examples/{s}", + .{entry.name}, + )), + .target = target, + .optimize = optimize, + }), + }); + exe.root_module.addImport("ink", b.modules.get("ink").?); + break :exe exe; + } else exe: { + const c_lib = c_lib_ orelse return error.UnsupportedPlatform; + const exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + }); + exe.linkLibC(); + exe.addIncludePath(b.path("include")); + exe.addCSourceFile(.{ + .file = b.path(b.fmt( + "examples/{s}", + .{entry.name}, + )), + .flags = &[_][]const u8{ + "-Wall", + "-Wextra", + "-pedantic", + "-std=c99", + "-D_POSIX_C_SOURCE=199309L", + }, + }); + exe.linkLibrary(c_lib); + break :exe exe; + }; + try steps.append(alloc, exe); + } + return steps.toOwnedSlice(alloc); +} diff --git a/examples/ink-c-driver.c b/examples/ink-c-driver.c new file mode 100644 index 0000000..9a6561b --- /dev/null +++ b/examples/ink-c-driver.c @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include + +#include + +struct option { + char *name; + int id; + bool has_args; +}; + +struct ink_source { + uint8_t *bytes; + size_t length; +}; + +enum { + OPTION_UNKNOWN = -2, + OPTION_OPERAND = -3 +}; + +enum { + OPT_COLORS = 1000, + OPT_COMPILE_ONLY, + OPT_USE_STDIN, + OPT_DUMP_AST, + OPT_DUMP_IR, + OPT_DUMP_STORY, + OPT_VM_TRACING, + OPT_HELP, +}; + +static const struct option cli_options[] = { + {"--colors", OPT_COLORS, false}, + {"--compile-only", OPT_COMPILE_ONLY, false}, + //{"--dump-ast", OPT_DUMP_AST, false}, + //{"--dump-ir", OPT_DUMP_AST, false}, + //{"--dump-story", OPT_DUMP_STORY, false}, + //{"--trace", OPT_VM_TRACING, false}, + {"--stdin", OPT_USE_STDIN, false}, + {"--help", OPT_HELP, false}, + {"-h", OPT_HELP, false}, + {0}, +}; + +static const char *usage_msg = + "Usage: %s [OPTION]... [FILE]\n" + "Load and execute an Ink story.\n\n" + " -h, --help Print this message\n" + " --colors Enable color output\n" + " --compile-only Compile the story without executing\n" + " --stdin Read source file from standard input\n" + "\n"; + +static const struct option *_g_option_opts; +static char *_g_arg_ptr; +static char **_g_option_argv; +static char *option_operand; +static char *option_unknown_opt; + +static void print_usage(const char *name) +{ + fprintf(stderr, usage_msg, name); +} + +static void option_setopts(const struct option *opts, char **argv) +{ + _g_option_argv = argv; + _g_option_opts = opts; +} + +static int option_nextopt(void) +{ + if (*_g_option_argv != NULL) { + ++_g_option_argv; + } else { + return 0; + } + + char **optstr_p = _g_option_argv; + if (*optstr_p == NULL) { + return 0; + } + + const struct option *p = _g_option_opts; + while (p->id != 0) { + if (!strcmp(*optstr_p, p->name)) { + if (p->has_args) { + _g_arg_ptr = optstr_p[1]; + ++_g_option_argv; + } + + return p->id; + } else if (**optstr_p != '-') { + option_operand = *optstr_p; + return OPTION_OPERAND; + } + + ++p; + } + + option_unknown_opt = *optstr_p; + return OPTION_UNKNOWN; +} + +static char *option_nextarg(void) +{ + char *arg = _g_arg_ptr; + char *arg_p = arg; + + while (*arg_p != '\0' && *arg_p != ',') { + ++arg_p; + } + if (*arg_p == '\0') { + _g_arg_ptr = arg_p; + return arg; + } + + _g_arg_ptr = arg_p + 1; + *arg_p = '\0'; + return arg; +} + +#define INK_SOURCE_BUF_MAX 1024 + +static const char *INK_FILE_EXT = ".ink"; +static const size_t INK_FILE_EXT_LENGTH = 4; + +static int ink_read_file(const char *file_path, uint8_t **bytes, size_t *length) +{ + size_t sz = 0, nr = 0; + uint8_t *b = NULL; + FILE *const fp = fopen(file_path, "rb"); + + if (!fp) { + fprintf(stderr, "Could not read file '%s'\n", file_path); + return -1; + } + + fseek(fp, 0u, SEEK_END); + sz = (size_t)ftell(fp); + fseek(fp, 0u, SEEK_SET); + + b = malloc(sz + 1); + if (!b) { + fclose(fp); + return -1; + } + + nr = fread(b, 1u, sz, fp); + if (nr < sz) { + fprintf(stderr, "Could not read file '%s'.\n", file_path); + fclose(fp); + free(b); + return -1; + } + + b[nr] = '\0'; + *bytes = b; + *length = sz; + fclose(fp); + return 0; +} + +static void ink_source_free(struct ink_source *s) +{ + free(s->bytes); + s->bytes = NULL; + s->length = 0; +} + +static int ink_source_load_stdin(struct ink_source *s) +{ + uint8_t *tmp; + char b[INK_SOURCE_BUF_MAX]; + + s->bytes = NULL; + s->length = 0; + + while (fgets(b, INK_SOURCE_BUF_MAX, stdin)) { + const size_t len = s->length; + const size_t buflen = strlen(b); + + tmp = realloc(s->bytes, len + buflen + 1); + if (!tmp) { + ink_source_free(s); + return -1; + } + + s->bytes = tmp; + memcpy(s->bytes + len, b, buflen); + s->length += buflen; + s->bytes[s->length] = '\0'; + } + if (ferror(stdin)) { + ink_source_free(s); + return -1; + } + return 0; +} + +static int ink_source_load(const char *file_path, struct ink_source *s) +{ + const char *ext; + const size_t namelen = strlen(file_path); + + s->bytes = NULL; + s->length = 0; + + if (namelen < INK_FILE_EXT_LENGTH) { + return -1; + } + + ext = file_path + namelen - INK_FILE_EXT_LENGTH; + if (!(strncmp(ext, INK_FILE_EXT, INK_FILE_EXT_LENGTH) == 0)) { + return -1; + } + return ink_read_file(file_path, &s->bytes, &s->length); +} + +int main(int argc, char *argv[]) +{ + bool compile_only = false; + bool use_stdin = false; + int opt = 0; + int rc = -1; + int flags = 0; + const char *filename = NULL; + struct ink_source source; + struct ink_story *story = NULL; + + option_setopts(cli_options, argv); + + while ((opt = option_nextopt())) { + switch (opt) { + case OPT_COLORS: + flags |= INK_F_USE_COLOR; + break; + case OPT_DUMP_AST: + flags |= INK_F_DUMP_AST; + break; + case OPT_DUMP_IR: + flags |= INK_F_DUMP_IR; + break; + case OPT_DUMP_STORY: + flags |= INK_F_DUMP_CODE; + break; + case OPT_COMPILE_ONLY: + compile_only = true; + break; + case OPT_USE_STDIN: + use_stdin = true; + break; + case OPTION_UNKNOWN: + fprintf(stderr, "Unrecognised option %s.\n\n", option_unknown_opt); + print_usage(argv[0]); + return EXIT_FAILURE; + case OPT_HELP: + print_usage(argv[0]); + return EXIT_SUCCESS; + case OPTION_OPERAND: + filename = option_operand; + break; + default: + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + if (filename == NULL || *filename == '\0') { + if (use_stdin) { + rc = ink_source_load_stdin(&source); + filename = ""; + } else { + print_usage(argv[0]); + return EXIT_FAILURE; + } + } else { + rc = ink_source_load(filename, &source); + } + if (rc < 0) { + fprintf(stderr, "Error while loading source file.\n"); + return rc; + } + + story = ink_open(); + if (!story) { + goto out; + } + + const struct ink_story_options opts = { + .source_bytes = source.bytes, + .source_length = source.length, + .filename = (uint8_t *)filename, + .filename_length = strlen(filename), + .flags = flags, + }; + + rc = ink_load_story_options(story, &opts); + if (rc < 0) { + goto out; + } + if (!compile_only) { + while (ink_story_can_continue(story)) { + const char *line = NULL; + size_t linelen = 0; + size_t choice_index = 0; + + while (ink_story_continue(story, &line, &linelen)) { + if (line) { + printf("%.*s\n", (int)linelen, line); + } + } + while (ink_story_choice_next(story, &line, &linelen)) { + printf("%zu: %.*s\n", ++choice_index, (int)linelen, line); + } + if (choice_index > 0) { + printf("> "); + scanf("%zu", &choice_index); + ink_story_choose(story, choice_index); + } + } + } +out: + ink_close(story); + ink_source_free(&source); + return EXIT_SUCCESS; +} diff --git a/include/ink.h b/include/ink.h new file mode 100644 index 0000000..f8c62ec --- /dev/null +++ b/include/ink.h @@ -0,0 +1,122 @@ +#ifndef INK_API_H +#define INK_API_H + +/** + * @defgroup ink_api API + * + * This module documents the exported functions that form the public API + * of libink. + * + * @{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) +#ifdef BUILDING_INKLIB +#define INK_API __declspec(dllexport) +#else +#define INK_API __declspec(dllimport) +#endif +#else +#if defined(__GNUC__) || defined(__clang__) +#define INK_API __attribute__((visibility("default"))) +#else +#define INK_API +#endif +#endif + +#include +#include +#include + +/** + * @struct ink_story + * + * @brief Opaque type representing a story context. + * + * This forward declaration hides the internal structure of an `ink_story`. + * Clients of the API should manipulate `ink_story` objects only through the + * provided functions. + */ +typedef struct ink_story ink_story; + +struct ink_story_options { + const uint8_t *filename; + size_t filename_length; + const uint8_t *source_bytes; + size_t source_length; + int flags; +}; + +enum ink_flags { + INK_F_RESERVED_1 = (1 << 0), + INK_F_RESERVED_2 = (1 << 1), + INK_F_RESERVED_3 = (1 << 2), + INK_F_RESERVED_4 = (1 << 3), + INK_F_USE_COLOR = (1 << 4), + INK_F_DUMP_AST = (1 << 5), + INK_F_DUMP_IR = (1 << 6), + INK_F_DUMP_CODE = (1 << 7), + INK_F_RESERVED_9 = (1 << 8), + INK_F_RESERVED_10 = (1 << 9), + INK_F_RESERVED_11 = (1 << 10), + INK_F_RESERVED_12 = (1 << 11), + INK_F_RESERVED_13 = (1 << 12), + INK_F_RESERVED_14 = (1 << 13), + INK_F_RESERVED_15 = (1 << 14), + INK_F_RESERVED_16 = (1 << 15), +}; + +/** + * @brief Open a story context. + * + * @returns a new story context + */ +INK_API struct ink_story *ink_open(void); + +/** + * Close a story context. + */ +INK_API void ink_close(struct ink_story *story); + +/** + * Load an Ink story with extended options. + * + * @returns a non-zero value on error. + */ +INK_API int ink_load_story_options(struct ink_story *story, + const struct ink_load_options *options); + +/** + * Determine if the story can continue. + * + * @returns a boolean value, indicating if the story can continue. + */ +INK_API bool ink_story_can_continue(struct ink_story *story); + +/** + * Advance the story and output content, if available. + * + * @returns a non-zero value on error. + */ +INK_API int ink_story_continue(struct ink_story *story, uint8_t **line, + size_t *linelen); + +INK_API int ink_story_choice_next(struct ink_story *story, uint8_t **line, + size_t *linelen); + +/** + * Select a choice by its index. + * + * @returns a non-zero value on error. + */ +INK_API int ink_story_choose(struct ink_story *story, size_t index); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/Story.zig b/src/Story.zig index fbde3ad..aaddba0 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -38,6 +38,7 @@ constants_pool: []const Value = &.{}, /// Interned string bytes. string_bytes: []const u8 = &.{}, dump_writer: ?*std.Io.Writer = null, +internal_counter: usize = 0, pub const default_knot_name: [:0]const u8 = "$__main__$"; @@ -372,23 +373,23 @@ pub fn deinit(story: *Story) void { story.* = undefined; } -fn currentFrame(vm: *Story) *CallFrame { +pub fn currentFrame(vm: *Story) *CallFrame { assert(vm.call_stack_top > 0); return &vm.call_stack[vm.call_stack_top - 1]; } -fn peekStack(vm: *Story, offset: usize) ?Value { +pub fn peekStack(vm: *Story, offset: usize) ?Value { if (vm.stack_top <= offset) return null; return vm.stack[vm.stack_top - offset - 1]; } -fn pushStack(vm: *Story, value: Value) !void { +pub fn pushStack(vm: *Story, value: Value) !void { if (vm.stack_top >= vm.stack.len) return error.StackOverflow; vm.stack[vm.stack_top] = value; vm.stack_top += 1; } -fn popStack(vm: *Story) ?Value { +pub fn popStack(vm: *Story) ?Value { if (vm.stack_top == 0) return null; const stack_top = vm.stack_top; @@ -489,7 +490,7 @@ fn divertValue(vm: *Story, value: Value, args_count: u8) !void { } // Diverts are essentially tail calls. -fn divert(vm: *Story, knot: *Object.Knot, args_count: u8) !void { +pub fn divert(vm: *Story, knot: *Object.Knot, args_count: u8) !void { assert(knot.code.args_count == args_count); if (vm.call_stack_top == 0) return vm.call(knot, args_count); diff --git a/src/c_api.zig b/src/c_api.zig new file mode 100644 index 0000000..062ac28 --- /dev/null +++ b/src/c_api.zig @@ -0,0 +1,158 @@ +const std = @import("std"); +const ink = @import("ink"); +const Story = ink.Story; +const Module = ink.Module; + +var global_allocator: std.heap.DebugAllocator(.{}) = .init; +var stdout_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; + +pub export fn ink_open() callconv(.c) ?*Story { + const gpa = global_allocator.allocator(); + const story = gpa.create(Story) catch |err| switch (err) { + error.OutOfMemory => return null, + }; + story.* = .{ + .gpa = gpa, + .arena = .init(gpa), + .is_exited = false, + .can_advance = false, + .stack_top = 0, + .call_stack_top = 0, + .output_marker = 0, + .choice_selected = null, + .output_buffer = .empty, + .output_scratch = .empty, + .current_choices = .empty, + .variable_observers = .empty, + .globals = .empty, + .stack = &.{}, + .call_stack = &.{}, + .code_chunks = .empty, + .gc_objects = .{}, + .constants_pool = &.{}, + .string_bytes = &.{}, + .dump_writer = null, + }; + return story; +} + +pub export fn ink_close(story: *Story) callconv(.c) void { + defer _ = global_allocator.deinit(); + const gpa = story.gpa; + story.deinit(); + gpa.destroy(story); +} + +pub const InkLoadOpts = extern struct { + filename: [*]const u8, + filename_length: usize, + source_bytes: [*]const u8, + source_length: usize, + flags: i32, +}; + +fn loadStory(story: *Story, options: *const InkLoadOpts) !void { + const gpa = story.gpa; + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + defer arena_allocator.deinit(); + + const arena = arena_allocator.allocator(); + const stderr = std.fs.File.stderr(); + var stderr_writer = stderr.writer(&stdout_buffer); + + var comp = try Module.compile(gpa, arena, .{ + .source_bytes = options.source_bytes[0..options.source_length :0], + .filename = options.filename[0..options.filename_length], + .dump_writer = null, + .dump_use_color = false, + .dump_ast = false, + .dump_ir = false, + }); + defer comp.deinit(); + + if (comp.errors.items.len > 0) { + for (comp.errors.items) |err| { + try comp.renderError(&stderr_writer.interface, err); + } + return error.LoadFailed; + } + + // TODO: Make this configureable. + const stack_size = 128; + const eval_stack_ptr = try gpa.alloc(Story.Value, stack_size); + errdefer gpa.free(eval_stack_ptr); + + const call_stack_ptr = try gpa.alloc(Story.CallFrame, stack_size); + errdefer gpa.free(call_stack_ptr); + + story.can_advance = false; + story.stack = eval_stack_ptr; + story.call_stack = call_stack_ptr; + errdefer story.deinit(); + + try comp.setupStoryRuntime(gpa, story); + if (story.getKnot(Story.default_knot_name)) |knot| { + try story.pushStack(.{ .object = &knot.base }); + try story.divert(knot, 0); + } +} + +pub export fn ink_load_story_options( + story: *Story, + options: *const InkLoadOpts, +) callconv(.c) c_int { + loadStory(story, options) catch |err| { + std.debug.print("{any}\n", .{@errorName(err)}); + return -1; + }; + return 0; +} + +pub export fn ink_story_can_continue(story: *Story) callconv(.c) bool { + return !story.is_exited and story.can_advance; +} + +pub export fn ink_story_continue( + story: *Story, + line: *?[*]const u8, + linelen: *usize, +) callconv(.c) c_int { + const result = story.advance() catch return -1; + if (result) |slice| { + line.* = slice.ptr; + linelen.* = slice.len; + return 1; + } else { + line.* = null; + linelen.* = 0; + return 0; + } +} + +// FIXME: The internal counter may be a bad idea... +pub export fn ink_story_choice_next( + story: *Story, + line: *?[*]const u8, + linelen: *usize, +) callconv(.c) c_int { + if (story.internal_counter < story.current_choices.items.len) { + const choice = &story.current_choices.items[story.internal_counter]; + line.* = choice.content.ptr; + linelen.* = choice.content.len; + story.internal_counter += 1; + return 1; + } else { + line.* = null; + linelen.* = 0; + return 0; + } +} + +// FIXME: The internal counter may be a bad idea... +pub export fn ink_story_choose(story: *Story, index: usize) callconv(.c) c_int { + story.selectChoiceIndex(index) catch { + return -1; + }; + story.internal_counter = 0; + return 0; +} diff --git a/src/root.zig b/src/root.zig index 0090fac..3aecb3b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tokenizer = @import("tokenizer.zig"); pub const Story = @import("Story.zig"); pub const Ast = @import("Ast.zig"); +pub const Module = @import("compile.zig").Module; pub const max_src_size = std.math.maxInt(u32);