From 97c82cd688057838d774bc1472e56df0dce1ad9c Mon Sep 17 00:00:00 2001 From: REMqb Date: Tue, 30 Apr 2024 01:47:30 +0200 Subject: [PATCH] + Build nzsl from sources --- .gitignore | 4 + build.zig | 228 +++++++++++++++++++++++++++++++++++++++ build.zig.bak | 91 ++++++++++++++++ build.zig.zon | 62 +++++++++++ examples/mandelbrot.nzsl | 60 +++++++++++ examples/mandelbrot.zig | 14 +++ src/lib.zig | 104 ++++++++++++++++++ src/main.zig | 24 +++++ src/nzsl-c.zig | 3 + src/root.zig | 10 ++ 10 files changed, 600 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.bak create mode 100644 build.zig.zon create mode 100644 examples/mandelbrot.nzsl create mode 100644 examples/mandelbrot.zig create mode 100644 src/lib.zig create mode 100644 src/main.zig create mode 100644 src/nzsl-c.zig create mode 100644 src/root.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d712b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +.idea/ +zig-cache/ +zig-out/ \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..5abfb3c --- /dev/null +++ b/build.zig @@ -0,0 +1,228 @@ +const std = @import("std"); + +const Build = std.Build; +const Step = std.Build.Step; + +pub const Libsource = enum { + source, + prebuild +}; + +// based on ziglua's build.zig + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *Build) void { + // Remove the default install and uninstall steps + b.top_level_steps = .{}; + + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const libsource = b.option(Libsource, "libsource", "Use prebuild or compile from sources") orelse .source; + const shared = b.option(bool, "shared", "Build shared library instead of static") orelse false; + + if (libsource == .prebuild) { + std.debug.panic("Prebuild aren't available for now", .{}); + } + + // Zig module + const nzslzig = b.addModule("nzslzig", .{ + .root_source_file = .{ .path = "src/lib.zig" }, + }); + + const docs = b.addStaticLibrary(.{ + .name = "nzslzig", + .root_source_file = .{ .path = "src/lib.zig" }, + .target = target, + .optimize = optimize, + }); + + // Expose build configuration to the nzslzig module + const config = b.addOptions(); + config.addOption(Libsource, "libsource", libsource); + nzslzig.addOptions("config", config); + + nzsldep: { + const upstream = b.lazyDependency(if (libsource == .source) "nzsl_source" else break :nzsldep , .{}) orelse break :nzsldep; + const nazaraUtils = b.lazyDependency("NazaraUtils", .{}) orelse break :nzsldep; + const frozen = b.lazyDependency("frozen", .{}) orelse break :nzsldep; + const fmt = b.lazyDependency("fmt", .{}) orelse break :nzsldep; + const ordered_map = b.lazyDependency("ordered_map", .{}) orelse break :nzsldep; + const fast_float = b.lazyDependency("fast_float", .{}) orelse break :nzsldep; + + const lib = switch (libsource) { + .source => buildNzsl(b, target, optimize, upstream, nazaraUtils, frozen, fmt, ordered_map, fast_float, shared), + else => unreachable, + }; + + // Expose the Nzsl artifact + b.installArtifact(lib); + + //nzslzig.addSystemIncludePath(upstream.path("include")); + + nzslzig.linkLibrary(lib); + docs.linkLibrary(lib); + } + + // Examples + const examples = [_]struct { []const u8, []const u8 }{ + .{ "mandelbrot", "examples/mandelbrot.zig" }, + }; + + for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example[0], + .root_source_file = .{ .path = example[1] }, + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("nzslzig", nzslzig); + + const artifact = b.addInstallArtifact(exe, .{}); + const exe_step = b.step(b.fmt("install-example-{s}", .{example[0]}), b.fmt("Install {s} example", .{example[0]})); + exe_step.dependOn(&artifact.step); + + 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(b.fmt("run-example-{s}", .{example[0]}), b.fmt("Run {s} example", .{example[0]})); + run_step.dependOn(&run_cmd.step); + } + + + docs.root_module.addOptions("config", config); + docs.root_module.addImport("nzslzig", nzslzig); + + const install_docs = b.addInstallDirectory(.{ + .source_dir = docs.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + + const docs_step = b.step("docs", "Build and install the documentation"); + docs_step.dependOn(&install_docs.step); +} + +fn buildNzsl(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, nazaraUtils: *Build.Dependency, frozen: *Build.Dependency, ordered_map: *Build.Dependency, fast_float: *Build.Dependency, fmt: *Build.Dependency, shared: bool) *Step.Compile { + + const lib_opts = .{ + .name = "nzsl", + .target = target, + .optimize = optimize, + }; + const lib = if (shared) + b.addSharedLibrary(lib_opts) + else + b.addStaticLibrary(lib_opts); + + lib.addSystemIncludePath(upstream.path("include")); + lib.addSystemIncludePath(upstream.path("src")); + + lib.addSystemIncludePath(nazaraUtils.path("include")); + lib.addSystemIncludePath(frozen.path("include")); + lib.addSystemIncludePath(fmt.path("include")); + lib.addSystemIncludePath(ordered_map.path("include")); + lib.addSystemIncludePath(fast_float.path("include")); + + // const includeFlag = std.fmt.allocPrint(b.allocator, "\"-isystem {s}\"", .{upstream.path("include").getPath(b)}) catch unreachable; + // defer b.allocator.free(includeFlag); + + const flags = [_][]const u8{ + // includeFlag + //"-I=" ++ .upstream.path(), + + if (shared) + "-DCNZSL_DYNAMIC" else "-DCNZSL_STATIC", + if (shared) "-DNZSL_DYNAMIC" else "-DNZSL_STATIC", + "-DCNZSL_BUILD", + "-DNZSL_BUILD", + "-DFMT_HEADER_ONLY", + + // Define target-specific macro + //switch (target.result.os.tag) { + //.linux => "-DLUA_USE_LINUX", + //.macos => "-DLUA_USE_MACOSX", + //.windows => "-DLUA_USE_WINDOWS", + //else => "-DLUA_USE_POSIX", + //}, + + // Enable api check + // if (optimize == .Debug) "-DLUA_USE_APICHECK" else "", + }; + + const nzsl_source_files = &nzsl_source_files_list; + + lib.addCSourceFiles(.{ + .root = .{ .dependency = .{ + .dependency = upstream, + .sub_path = "", + } }, + .files = nzsl_source_files, + .flags = &flags, + }); + + lib.linkLibCpp(); + + lib.installHeader(upstream.path("include/CNZSL/CNZSL.h"), "CNZSL/CNZSL.h"); + lib.installHeader(upstream.path("include/CNZSL/Config.h"), "CNZSL/Config.h"); + lib.installHeader(upstream.path("include/CNZSL/DebugLevel.h"), "CNZSL/DebugLevel.h"); + lib.installHeader(upstream.path("include/CNZSL/GlslWriter.h"), "CNZSL/GlslWriter.h"); + lib.installHeader(upstream.path("include/CNZSL/LangWriter.h"), "CNZSL/LangWriter.h"); + lib.installHeader(upstream.path("include/CNZSL/Module.h"), "CNZSL/Module.h"); + lib.installHeader(upstream.path("include/CNZSL/Parser.h"), "CNZSL/Parser.h"); + lib.installHeader(upstream.path("include/CNZSL/ShaderStageType.h"), "CNZSL/ShaderStageType.h"); + lib.installHeader(upstream.path("include/CNZSL/SpirvWriter.h"), "CNZSL/SpirvWriter.h"); + lib.installHeader(upstream.path("include/CNZSL/WriterStates.h"), "CNZSL/WriterStates.h"); + + return lib; +} + +const nzsl_source_files_list = [_][]const u8{ + "src/NZSL/Ast/AstSerializer.cpp", + "src/NZSL/Ast/Cloner.cpp", + "src/NZSL/Ast/ConstantPropagationVisitor.cpp", + "src/NZSL/Ast/ConstantPropagationVisitor_BinaryArithmetics.cpp", + "src/NZSL/Ast/ConstantPropagationVisitor_BinaryComparison.cpp", + "src/NZSL/Ast/ConstantValue.cpp", + "src/NZSL/Ast/DependencyCheckerVisitor.cpp", + "src/NZSL/Ast/EliminateUnusedPassVisitor.cpp", + "src/NZSL/Ast/ExportVisitor.cpp", + "src/NZSL/Ast/ExpressionType.cpp", + "src/NZSL/Ast/ExpressionVisitor.cpp", + "src/NZSL/Ast/ExpressionVisitorExcept.cpp", + "src/NZSL/Ast/IndexRemapperVisitor.cpp", + "src/NZSL/Ast/Nodes.cpp", + "src/NZSL/Ast/RecursiveVisitor.cpp", + "src/NZSL/Ast/ReflectVisitor.cpp", + "src/NZSL/Ast/SanitizeVisitor.cpp", + "src/NZSL/Ast/StatementVisitor.cpp", + "src/NZSL/Ast/StatementVisitorExcept.cpp", + "src/NZSL/Ast/Utils.cpp", + "src/NZSL/FilesystemModuleResolver.cpp", + "src/NZSL/GlslWriter.cpp", + "src/NZSL/Lang/Errors.cpp", + "src/NZSL/LangWriter.cpp", + "src/NZSL/Lexer.cpp", + "src/NZSL/ModuleResolver.cpp", + "src/NZSL/Parser.cpp", + "src/NZSL/Serializer.cpp", + "src/NZSL/ShaderWriter.cpp", + "src/NZSL/SpirV/SpirvAstVisitor.cpp", + "src/NZSL/SpirV/SpirvConstantCache.cpp", + "src/NZSL/SpirV/SpirvData.cpp", + "src/NZSL/SpirV/SpirvDecoder.cpp", + "src/NZSL/SpirV/SpirvExpressionLoad.cpp", + "src/NZSL/SpirV/SpirvExpressionStore.cpp", + "src/NZSL/SpirV/SpirvPrinter.cpp", + "src/NZSL/SpirV/SpirvSectionBase.cpp", + "src/NZSL/SpirvWriter.cpp", + "src/CNZSL/GlslWriter.cpp", + "src/CNZSL/LangWriter.cpp", + "src/CNZSL/Module.cpp", + "src/CNZSL/Parser.cpp", + "src/CNZSL/SpirvWriter.cpp", + "src/CNZSL/WriterStates.cpp", +}; \ No newline at end of file diff --git a/build.zig.bak b/build.zig.bak new file mode 100644 index 0000000..b3a1a85 --- /dev/null +++ b/build.zig.bak @@ -0,0 +1,91 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "nzsl-zig", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + const exe = b.addExecutable(.{ + .name = "nzsl-zig", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d26c720 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,62 @@ +.{ + .name = "nzsl-zig", + // 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", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // 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 = .{ + .nzsl_source = .{ + .url = "https://github.com/NazaraEngine/ShaderLang/archive/ddbc7b179b2fdcb82dcd20e4aa8a85dfede0891b.tar.gz", + .hash = "12208c569d8bc21e0412c17da081ee7944c1c3b2de9c6bcfa1863ded4af2faad0cf0", + .lazy = true, + }, + .NazaraUtils = .{ + .url = "https://github.com/NazaraEngine/NazaraUtils/archive/cbdb331a9610fa52f51e7c86576cc9e2e7235691.tar.gz", + .hash = "1220a6576ccd4e2c0cf5ab804ef680c4d1834859d1f7a7e3228fef73f8dce8c0e826", + .lazy = true, + }, + .frozen = .{ + .url = "https://github.com/serge-sans-paille/frozen/archive/292a8110466ae964c8fde9d0e0371e5ac21bfa60.tar.gz", + .hash = "122039e95c3739bd73b8413d3448395b1e7b17953e342e4c3c07de848d406a28733c", + .lazy = true, + }, + .fmt = .{ + .url = "https://github.com/fmtlib/fmt/archive/e69e5f977d458f2650bb346dadf2ad30c5320281.tar.gz", + .hash = "12202f41bb1cbca6f555a37883ce510374b9ca577ed5cd117d71c7bdc2c87380e14d", + .lazy = true, + }, + .ordered_map = .{ + .url = "https://github.com/Tessil/ordered-map/archive/bd8d5ef4149cd40783a486011778a2e7eedde441.tar.gz", + .hash = "122079b9af72c89c53a2fae29968665d7c8c5c9c466764019b7bf81f605072bd087f", + .lazy = true, + }, + .fast_float = .{ + .url = "https://github.com/fastfloat/fast_float/archive/1fc3ac3932220b5effaca7203bb1bb771528d256.tar.gz", + .hash = "12201d7ec3e6bd8e0e90d5792a66fd4ccc670f337ee8818f8d830c1f4bd7e2fe2f3b", + .lazy = true, + }, + }, + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/examples/mandelbrot.nzsl b/examples/mandelbrot.nzsl new file mode 100644 index 0000000..7187cad --- /dev/null +++ b/examples/mandelbrot.nzsl @@ -0,0 +1,60 @@ +[nzsl_version("1.0")] +[desc("Mandelbrot shader from http://nuclear.mutantstargoat.com/articles/sdr_fract")] +[feature(primitive_externals)] //< Required since SFML doesn't use UBO +module; + +external +{ + [binding(0)] palette: sampler2D[f32], + [binding(1)] screen_size: vec2[f32], + [binding(2)] center: vec2[f32], + [binding(3)] scale: f32, + [binding(4)] iteration_count: i32 +} + +struct Input +{ + [builtin(frag_coord)] fragcoord: vec4[f32] +} + +struct Output +{ + [location(0)] color: vec4[f32] +} + +[entry(frag)] +fn main(input: Input) -> Output +{ + let coords = input.fragcoord.xy / screen_size; + + let c: vec2[f32]; + c.x = (screen_size.x / screen_size.y) * (coords.x - 0.5) * scale - center.x / screen_size.y; + c.y = (coords.y - 0.5) * scale - center.y / screen_size.y; + + let z = c; + let i = 0; + while (i < iteration_count) + { + let x = (z.x * z.x - z.y * z.y) + c.x; + let y = (z.y * z.x + z.x * z.y) + c.y; + + if ((x * x + y * y) > 4.0) + break; + + z.x = x; + z.y = y; + + i += 1; + } + + let u: f32; + if (i < iteration_count) + u = f32(i) / 100.0; + else + u = 0.0; + + let out: Output; + out.color = palette.Sample(vec2[f32](u, 0.0)); + + return out; +} diff --git a/examples/mandelbrot.zig b/examples/mandelbrot.zig new file mode 100644 index 0000000..230c84e --- /dev/null +++ b/examples/mandelbrot.zig @@ -0,0 +1,14 @@ +const std= @import("std"); +const nzsl = @import("nzslzig"); + +pub fn main() !void { + const mandelbrotModule = try nzsl.parseFromFile("./mandelbrot.nzsl"); + defer mandelbrotModule.release(); + + const glslWriter = nzsl.GlslWriter.create(); + defer glslWriter.release(); + + const output = try glslWriter.generate(mandelbrotModule); + defer output.release(); + +} \ No newline at end of file diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 0000000..813556e --- /dev/null +++ b/src/lib.zig @@ -0,0 +1,104 @@ +//const nzsl = @cImport({ +// @cInclude("CNZSL/CNZSL.h"); +//}); +const std = @import("std"); +const cnzsl = @import("cimport.zig"); + +pub fn parseSource(source: []const u8) !Module { + const module = Module{.instance = null}; + + if(cnzsl.nzslParserParseSource(module.instance, source.ptr, source.len) != 0) { + defer module.release(); + std.log.err("Error while parsing source: {s}", .{ module.getLastError() }); + + return error.FailedToParse; + } + + return module; +} + +pub fn parseSourceWithFilePath(source: []const u8, filePath: []const u8) !Module { + const module: ?*cnzsl.nzslModule = null; + + if(cnzsl.nzslParserParseSourceWithFilePath(module, source.ptr, source.len, filePath.ptr, filePath.len) != 0) { + defer cnzsl.nzslModuleDestroy(module); + std.log.err("Error while parsing source: {s}", .{ module.getLastError() }); + + return error.FailedToParse; + } + + if (module == null) { + std.log.err("Error while parsing source: Unknown error", .{ }); + return error.FailedToParse; + } + + return .{.instance = module}; +} + +pub fn parseFromFile(filePath: []const u8) !Module { + const module: ?*cnzsl.nzslModule = null; + + if(cnzsl.nzslParserParseFromFile(module, filePath.ptr, filePath.len) != 0) { + defer cnzsl.nzslModuleDestroy(module); + std.log.err("Error while parsing source: {s}", .{ cnzsl.nzslModuleGetLastError(module) }); + + return error.FailedToParse; + } + + if (module == null) { + std.log.err("Error while parsing source: Unknown error", .{ }); + return error.FailedToParse; + } + + return .{.instance = module}; +} + +pub const Module = struct { + instance: *cnzsl.nzslModule, + + pub fn release(self: Module) void { + cnzsl.nzslModuleDestroy(self.instance); + } + + pub fn getLastError(self: Module) [*c]const u8 { + return cnzsl.nzslModuleGetLastError(self.instance); + } +}; + +pub const GlslWriter = struct { + instance: *cnzsl.nzslGlslWriter, + + pub fn create() GlslWriter { + return .{.instance = cnzsl.nzslGlslWriterCreate() orelse unreachable}; + } + + pub fn release(self: GlslWriter) void { + cnzsl.nzslGlslWriterDestroy(self.instance); + } + + pub fn generate(self: GlslWriter, module: Module) !GlslOutput { + var output: ?*cnzsl.nzslGlslOutput = null; + + output = cnzsl.nzslGlslWriterGenerate(self.instance, module.instance, null, null); + + if(output == null) { + std.log.err("Failed to generate glsl output: {s}", .{ self.getLastError() }); + + return error.FailedToGenerateGlsl; + } + + return .{.instance = output orelse unreachable}; + } + + pub fn getLastError(self: GlslWriter) [*c]const u8 { + return cnzsl.nzslGlslWriterGetLastError(self.instance); + } +}; + +pub const GlslOutput = struct { + instance: *cnzsl.nzslGlslOutput, + + pub fn release(self: GlslOutput) void { + cnzsl.nzslGlslOutputDestroy(self.instance); + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c8a3f67 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn main() !void { + // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + + // stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try bw.flush(); // don't forget to flush! +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} diff --git a/src/nzsl-c.zig b/src/nzsl-c.zig new file mode 100644 index 0000000..09278f3 --- /dev/null +++ b/src/nzsl-c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("CNZSL/CNZSL.h"); +}); \ No newline at end of file diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ecfeade --- /dev/null +++ b/src/root.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const testing = std.testing; + +export fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try testing.expect(add(3, 7) == 10); +}