diff --git a/.github/workflows/provable.yml b/.github/workflows/provable.yml new file mode 100644 index 0000000..b5269c4 --- /dev/null +++ b/.github/workflows/provable.yml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MPL-2.0 +# Provable — machine-checks the claims TypedQLiser makes beyond unit tests. +# +# 1. golden-check — the type-checker ACCEPTS valid SQL and REJECTS invalid SQL +# (the blog-api example: good-queries pass, bad-queries fail at L1/L2/L3). +# 2. zig-ffi — the Zig C-ABI FFI builds and its unit tests pass. +# +# The Rust layer itself is covered by rust-ci.yml. +name: Provable + +on: + push: + branches: [main, master, "claude/**"] + pull_request: + +permissions: + contents: read + +jobs: + golden-check: + name: TypedQL — accepts good SQL, rejects bad + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Build + run: cargo build --quiet + - name: Positive — a valid query type-checks (exit 0) + working-directory: examples/blog-api + run: >- + ../../target/debug/typedqliser check + -q 'SELECT id, username, email FROM users WHERE id = $1' --ci + - name: Negative — invalid queries are rejected (exit non-zero) + working-directory: examples/blog-api + run: | + set -x + # syntax error (L1) must be rejected + ! ../../target/debug/typedqliser check -q 'SELEC * FORM users' --ci + # nonexistent column (L2) must be rejected + ! ../../target/debug/typedqliser check -q 'SELECT id, full_name FROM users' --ci + - name: Full example — good passes, bad fails + working-directory: examples/blog-api + run: | + out=$(../../target/debug/typedqliser check 2>&1 || true) + echo "$out" + echo "$out" | grep -q '2 queries checked, 1 passed, 1 failed' + + zig-ffi: + name: Zig — build + test FFI + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: mlugg/setup-zig@53fc45b17fe98b52f92ee5ea08ff48a85a3e7eb7 # v1.2.2 + with: + version: 0.14.0 + - name: zig build + test + working-directory: src/interface/ffi + run: | + zig version + zig build test diff --git a/.gitignore b/.gitignore index f522aef..4b77703 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,11 @@ Thumbs.db # Rust # Cargo.lock # Keep for binaries +# Zig (FFI build artifacts from `zig build`) +zig-out/ +.zig-cache/ +zig-cache/ + # Elixir /cover/ /doc/ diff --git a/Cargo.lock b/Cargo.lock index f3310b1..ae240d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -105,12 +111,51 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.0" @@ -157,12 +202,85 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "equivalent" version = "1.0.2" @@ -200,6 +318,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -219,6 +361,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -240,6 +393,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "id-arena" version = "2.3.0" @@ -258,18 +417,49 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -327,6 +517,46 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "predicates" version = "3.1.4" @@ -391,6 +621,26 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.12.3" @@ -433,6 +683,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "same-file" version = "1.0.6" @@ -500,6 +756,12 @@ dependencies = [ "serde", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "sqlparser" version = "0.53.0" @@ -565,6 +827,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "toml" version = "0.8.23" @@ -613,6 +885,7 @@ dependencies = [ "anyhow", "assert_cmd", "clap", + "criterion", "glob", "predicates", "serde", @@ -679,6 +952,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -713,6 +1031,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -834,6 +1162,26 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/README.adoc b/README.adoc index c3f62c0..c0beea6 100644 --- a/README.adoc +++ b/README.adoc @@ -159,14 +159,16 @@ skip = [9, 10] == Architecture -=== Idris2 Type Kernel (`src/abi/`) +=== Level model — proof obligations (`src/codegen`, `src/plugins`) -The core of TypedQLiser is an Idris2 type kernel that encodes the 10 levels -as dependent types. Each level is a *proof obligation* — the kernel either -constructs a proof term (query is safe at that level) or produces a -counterexample (here's how it could fail). +The 10 safety levels are specified as *proof obligations*: for each level the +checker either accepts the query (safe at that level) or produces a +counterexample (how it could fail). These obligations are the design target for +a future Idris2 dependent-type kernel; **today they are enforced operationally +in Rust** (`src/codegen` + the language plugins), not yet mechanised in Idris2. +`src/abi/` holds the Rust ABI types. -Key proofs: +Proof obligations (one per level): * **ParseSafe**: `parse(q) = Right ast` (total parser, no panics) * **SchemaBound**: `∀ ref ∈ ast. ref ∈ schema` (all references resolve) * **TypeCompat**: `∀ op ∈ ast. types(op.lhs) ≡ types(op.rhs)` (no type mismatches) @@ -178,10 +180,11 @@ Key proofs: * **TemporalSafe**: `valid_at(q, now()) = True` (time-dependent predicates hold) * **LinearSafe**: `consumed(resources(q)) = exactly_once` (no leaks, no double-use) -=== Zig FFI (`ffi/zig/`) +=== Zig FFI (`src/interface/ffi/`) -C-ABI bridge between the Idris2 type kernel and the Rust CLI. -Handles proof serialisation and AST marshalling. +C-ABI bridge exposing TypedQLiser to other languages (build + unit tests +machine-checked in CI via `provable.yml`). The C header and proof-serialisation +marshalling are still being filled in. === Plugin Architecture (`src/plugins/`) @@ -243,8 +246,11 @@ typedqliser info == Status -**Pre-alpha.** Architecture defined, plugin trait designed, Idris2 proof -obligations specified. SQL and GraphQL plugins are priority implementations. +**Pre-alpha.** The Rust type-checker is real and tested (116 tests); the SQL +plugin enforces levels 1–5 — see the blog-api example, machine-checked in CI via +`provable.yml` (valid SQL accepted, invalid SQL rejected). The Idris2 +dependent-type kernel is *specified but not yet implemented* — levels are +currently enforced in Rust. GraphQL and further plugins are next. This is the **#1 priority** -iser project. diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig index 4a2e049..a6bb943 100644 --- a/src/interface/ffi/build.zig +++ b/src/interface/ffi/build.zig @@ -1,4 +1,4 @@ -// {{PROJECT}} FFI Build Configuration +// TypedQLiser FFI Build Configuration // SPDX-License-Identifier: MPL-2.0 const std = @import("std"); @@ -7,42 +7,37 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // Shared library (.so, .dylib, .dll) + // Shared library (.so, .dylib, .dll). + // No explicit `.version` — a versioned shared library trips a null deref in + // Zig 0.14's InstallArtifact (major_only_filename). const lib = b.addSharedLibrary(.{ - .name = "{{project}}", + .name = "typedqliser", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); - - // Set version - lib.version = .{ .major = 0, .minor = 1, .patch = 0 }; + lib.linkLibC(); // main.zig uses std.heap.c_allocator // Static library (.a) const lib_static = b.addStaticLibrary(.{ - .name = "{{project}}", + .name = "typedqliser", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + lib_static.linkLibC(); // Install artifacts b.installArtifact(lib); b.installArtifact(lib_static); - // Generate header file for C compatibility - const header = b.addInstallHeader( - b.path("include/{{project}}.h"), - "{{project}}.h", - ); - b.getInstallStep().dependOn(&header.step); - // Unit tests const lib_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + lib_tests.linkLibC(); const run_lib_tests = b.addRunArtifact(lib_tests); @@ -55,7 +50,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - + integration_tests.linkLibC(); integration_tests.linkLibrary(lib); const run_integration_tests = b.addRunArtifact(integration_tests); @@ -69,6 +64,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = .Debug, }); + docs.linkLibC(); const docs_step = b.step("docs", "Generate documentation"); docs_step.dependOn(&b.addInstallDirectory(.{ @@ -79,12 +75,12 @@ pub fn build(b: *std.Build) void { // Benchmark (if needed) const bench = b.addExecutable(.{ - .name = "{{project}}-bench", + .name = "typedqliser-bench", .root_source_file = b.path("bench/bench.zig"), .target = target, .optimize = .ReleaseFast, }); - + bench.linkLibC(); bench.linkLibrary(lib); const run_bench = b.addRunArtifact(bench); diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index 6b233bc..8187440 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -1,15 +1,13 @@ -// {{PROJECT}} FFI Implementation -// -// This module implements the C-compatible FFI declared in src/abi/Foreign.idr -// All types and layouts must match the Idris2 ABI definitions. -// // SPDX-License-Identifier: MPL-2.0 +// TypedQLiser FFI Implementation +// +// C-ABI FFI surface for embedding TypedQLiser in other languages. const std = @import("std"); // Version information (keep in sync with project) const VERSION = "0.1.0"; -const BUILD_INFO = "{{PROJECT}} built with Zig " ++ @import("builtin").zig_version_string; +const BUILD_INFO = "TypedQLiser built with Zig " ++ @import("builtin").zig_version_string; /// Thread-local error storage threadlocal var last_error: ?[]const u8 = null; @@ -37,8 +35,9 @@ pub const Result = enum(c_int) { null_pointer = 4, }; -/// Library handle (opaque to prevent direct access) -pub const Handle = opaque { +/// Library handle — a struct internally; C only ever sees `*Handle` as an +/// opaque pointer (its fields are never exposed in the header). +pub const Handle = struct { // Internal state hidden from C allocator: std.mem.Allocator, initialized: bool, @@ -51,7 +50,7 @@ pub const Handle = opaque { /// Initialize the library /// Returns a handle, or null on failure -export fn {{project}}_init() ?*Handle { +export fn typedqliser_init() ?*Handle { const allocator = std.heap.c_allocator; const handle = allocator.create(Handle) catch { @@ -70,7 +69,7 @@ export fn {{project}}_init() ?*Handle { } /// Free the library handle -export fn {{project}}_free(handle: ?*Handle) void { +export fn typedqliser_free(handle: ?*Handle) void { const h = handle orelse return; const allocator = h.allocator; @@ -86,7 +85,7 @@ export fn {{project}}_free(handle: ?*Handle) void { //============================================================================== /// Process data (example operation) -export fn {{project}}_process(handle: ?*Handle, input: u32) Result { +export fn typedqliser_process(handle: ?*Handle, input: u32) Result { const h = handle orelse { setError("Null handle"); return .null_pointer; @@ -110,7 +109,7 @@ export fn {{project}}_process(handle: ?*Handle, input: u32) Result { /// Get a string result (example) /// Caller must free the returned string -export fn {{project}}_get_string(handle: ?*Handle) ?[*:0]const u8 { +export fn typedqliser_get_string(handle: ?*Handle) ?[*:0]const u8 { const h = handle orelse { setError("Null handle"); return null; @@ -132,7 +131,7 @@ export fn {{project}}_get_string(handle: ?*Handle) ?[*:0]const u8 { } /// Free a string allocated by the library -export fn {{project}}_free_string(str: ?[*:0]const u8) void { +export fn typedqliser_free_string(str: ?[*:0]const u8) void { const s = str orelse return; const allocator = std.heap.c_allocator; @@ -145,7 +144,7 @@ export fn {{project}}_free_string(str: ?[*:0]const u8) void { //============================================================================== /// Process an array of data -export fn {{project}}_process_array( +export fn typedqliser_process_array( handle: ?*Handle, buffer: ?[*]const u8, len: u32, @@ -181,7 +180,7 @@ export fn {{project}}_process_array( /// Get the last error message /// Returns null if no error -export fn {{project}}_last_error() ?[*:0]const u8 { +export fn typedqliser_last_error() ?[*:0]const u8 { const err = last_error orelse return null; // Return C string (static storage, no need to free) @@ -195,12 +194,12 @@ export fn {{project}}_last_error() ?[*:0]const u8 { //============================================================================== /// Get the library version -export fn {{project}}_version() [*:0]const u8 { +export fn typedqliser_version() [*:0]const u8 { return VERSION.ptr; } /// Get build information -export fn {{project}}_build_info() [*:0]const u8 { +export fn typedqliser_build_info() [*:0]const u8 { return BUILD_INFO.ptr; } @@ -212,7 +211,7 @@ export fn {{project}}_build_info() [*:0]const u8 { pub const Callback = *const fn (u64, u32) callconv(.C) u32; /// Register a callback -export fn {{project}}_register_callback( +export fn typedqliser_register_callback( handle: ?*Handle, callback: ?Callback, ) Result { @@ -243,7 +242,7 @@ export fn {{project}}_register_callback( //============================================================================== /// Check if handle is initialized -export fn {{project}}_is_initialized(handle: ?*Handle) u32 { +export fn typedqliser_is_initialized(handle: ?*Handle) u32 { const h = handle orelse return 0; return if (h.initialized) 1 else 0; } @@ -253,22 +252,22 @@ export fn {{project}}_is_initialized(handle: ?*Handle) u32 { //============================================================================== test "lifecycle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); - try std.testing.expect({{project}}_is_initialized(handle) == 1); + try std.testing.expect(typedqliser_is_initialized(handle) == 1); } test "error handling" { - const result = {{project}}_process(null, 0); + const result = typedqliser_process(null, 0); try std.testing.expectEqual(Result.null_pointer, result); - const err = {{project}}_last_error(); + const err = typedqliser_last_error(); try std.testing.expect(err != null); } test "version" { - const ver = {{project}}_version(); + const ver = typedqliser_version(); const ver_str = std.mem.span(ver); try std.testing.expectEqualStrings(VERSION, ver_str); } diff --git a/src/interface/ffi/test/integration_test.zig b/src/interface/ffi/test/integration_test.zig index 0341994..1f6ee89 100644 --- a/src/interface/ffi/test/integration_test.zig +++ b/src/interface/ffi/test/integration_test.zig @@ -1,42 +1,42 @@ -// {{PROJECT}} Integration Tests // SPDX-License-Identifier: MPL-2.0 +// TypedQLiser Integration Tests // -// These tests verify that the Zig FFI correctly implements the Idris2 ABI +// These tests exercise the Zig FFI's C-ABI surface. const std = @import("std"); const testing = std.testing; // Import FFI functions -extern fn {{project}}_init() ?*opaque {}; -extern fn {{project}}_free(?*opaque {}) void; -extern fn {{project}}_process(?*opaque {}, u32) c_int; -extern fn {{project}}_get_string(?*opaque {}) ?[*:0]const u8; -extern fn {{project}}_free_string(?[*:0]const u8) void; -extern fn {{project}}_last_error() ?[*:0]const u8; -extern fn {{project}}_version() [*:0]const u8; -extern fn {{project}}_is_initialized(?*opaque {}) u32; +extern fn typedqliser_init() ?*opaque {}; +extern fn typedqliser_free(?*opaque {}) void; +extern fn typedqliser_process(?*opaque {}, u32) c_int; +extern fn typedqliser_get_string(?*opaque {}) ?[*:0]const u8; +extern fn typedqliser_free_string(?[*:0]const u8) void; +extern fn typedqliser_last_error() ?[*:0]const u8; +extern fn typedqliser_version() [*:0]const u8; +extern fn typedqliser_is_initialized(?*opaque {}) u32; //============================================================================== // Lifecycle Tests //============================================================================== test "create and destroy handle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); try testing.expect(handle != null); } test "handle is initialized" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); - const initialized = {{project}}_is_initialized(handle); + const initialized = typedqliser_is_initialized(handle); try testing.expectEqual(@as(u32, 1), initialized); } test "null handle is not initialized" { - const initialized = {{project}}_is_initialized(null); + const initialized = typedqliser_is_initialized(null); try testing.expectEqual(@as(u32, 0), initialized); } @@ -45,15 +45,15 @@ test "null handle is not initialized" { //============================================================================== test "process with valid handle" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); - const result = {{project}}_process(handle, 42); + const result = typedqliser_process(handle, 42); try testing.expectEqual(@as(c_int, 0), result); // 0 = ok } test "process with null handle returns error" { - const result = {{project}}_process(null, 42); + const result = typedqliser_process(null, 42); try testing.expectEqual(@as(c_int, 4), result); // 4 = null_pointer } @@ -62,17 +62,17 @@ test "process with null handle returns error" { //============================================================================== test "get string result" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); - const str = {{project}}_get_string(handle); - defer if (str) |s| {{project}}_free_string(s); + const str = typedqliser_get_string(handle); + defer if (str) |s| typedqliser_free_string(s); try testing.expect(str != null); } test "get string with null handle" { - const str = {{project}}_get_string(null); + const str = typedqliser_get_string(null); try testing.expect(str == null); } @@ -81,9 +81,9 @@ test "get string with null handle" { //============================================================================== test "last error after null handle operation" { - _ = {{project}}_process(null, 0); + _ = typedqliser_process(null, 0); - const err = {{project}}_last_error(); + const err = typedqliser_last_error(); try testing.expect(err != null); if (err) |e| { @@ -93,10 +93,10 @@ test "last error after null handle operation" { } test "no error after successful operation" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); - _ = {{project}}_process(handle, 0); + _ = typedqliser_process(handle, 0); // Error should be cleared after successful operation // (This depends on implementation) @@ -107,14 +107,14 @@ test "no error after successful operation" { //============================================================================== test "version string is not empty" { - const ver = {{project}}_version(); + const ver = typedqliser_version(); const ver_str = std.mem.span(ver); try testing.expect(ver_str.len > 0); } test "version string is semantic version format" { - const ver = {{project}}_version(); + const ver = typedqliser_version(); const ver_str = std.mem.span(ver); // Should be in format X.Y.Z @@ -126,28 +126,28 @@ test "version string is semantic version format" { //============================================================================== test "multiple handles are independent" { - const h1 = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(h1); + const h1 = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(h1); - const h2 = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(h2); + const h2 = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(h2); try testing.expect(h1 != h2); // Operations on h1 should not affect h2 - _ = {{project}}_process(h1, 1); - _ = {{project}}_process(h2, 2); + _ = typedqliser_process(h1, 1); + _ = typedqliser_process(h2, 2); } test "double free is safe" { - const handle = {{project}}_init() orelse return error.InitFailed; + const handle = typedqliser_init() orelse return error.InitFailed; - {{project}}_free(handle); - {{project}}_free(handle); // Should not crash + typedqliser_free(handle); + typedqliser_free(handle); // Should not crash } test "free null is safe" { - {{project}}_free(null); // Should not crash + typedqliser_free(null); // Should not crash } //============================================================================== @@ -155,8 +155,8 @@ test "free null is safe" { //============================================================================== test "concurrent operations" { - const handle = {{project}}_init() orelse return error.InitFailed; - defer {{project}}_free(handle); + const handle = typedqliser_init() orelse return error.InitFailed; + defer typedqliser_free(handle); const ThreadContext = struct { h: *opaque {}, @@ -165,7 +165,7 @@ test "concurrent operations" { const thread_fn = struct { fn run(ctx: ThreadContext) void { - _ = {{project}}_process(ctx.h, ctx.id); + _ = typedqliser_process(ctx.h, ctx.id); } }.run;