diff --git a/LICENSE b/LICENSE index 14e2f77..ec540b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,373 +1,153 @@ -Mozilla Public License Version 2.0 -================================== +SPDX-License-Identifier: MPL-2.0 +SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council + +================================================================================ +PALIMPSEST-MPL LICENSE VERSION 1.0 +================================================================================ -1. Definitions --------------- +File-level copyleft with ethical use and quantum-safe provenance + +Based on Mozilla Public License 2.0 -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. +-------------------------------------------------------------------------------- +PREAMBLE +-------------------------------------------------------------------------------- -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. +This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions +for ethical use, post-quantum cryptographic provenance, and emotional lineage +protection. The base MPL-2.0 terms apply except where explicitly modified by +the Exhibits below. + +Like a palimpsest manuscript where each layer builds upon what came before, +this license recognizes that creative works carry history, context, and meaning +that transcend mere code or text. + +-------------------------------------------------------------------------------- +SECTION 1: BASE LICENSE +-------------------------------------------------------------------------------- + +This License incorporates the full text of Mozilla Public License 2.0 by +reference. The complete MPL-2.0 text is available at: +https://www.mozilla.org/en-US/MPL/2.0/ + +All terms, conditions, and definitions from MPL-2.0 apply except where +explicitly modified by the Exhibits in this License. -1.3. "Contribution" - means Covered Software of a particular Contributor. +-------------------------------------------------------------------------------- +SECTION 2: ADDITIONAL DEFINITIONS +-------------------------------------------------------------------------------- -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. +2.1. "Emotional Lineage" + means the narrative, cultural, symbolic, and contextual meaning embedded + in Covered Software, including but not limited to: protest traditions, + cultural heritage, trauma narratives, and community stories. + +2.2. "Provenance Metadata" + means cryptographically signed attribution information attached to or + associated with Covered Software, including author identities, timestamps, + modification history, and lineage references. + +2.3. "Non-Interpretive System" + means any automated system that processes Covered Software without + preserving or considering its Emotional Lineage, including but not + limited to: AI training pipelines, content aggregators, and automated + summarization tools. + +2.4. "Quantum-Safe Signature" + means a cryptographic signature using algorithms resistant to attacks + by quantum computers, as specified in Exhibit B. + +-------------------------------------------------------------------------------- +SECTION 3: ETHICAL USE REQUIREMENTS +-------------------------------------------------------------------------------- + +In addition to the rights and obligations under MPL-2.0: + +3.1. Emotional Lineage Preservation + You must make reasonable efforts to preserve and communicate the + Emotional Lineage of Covered Software when distributing or creating + derivative works. This includes maintaining narrative context, cultural + attributions, and symbolic meaning where documented. + +3.2. Non-Interpretive System Notice + If You use Covered Software as input to a Non-Interpretive System, You + must: + (a) document such use in a publicly accessible manner; and + (b) not claim that outputs of such systems carry the Emotional Lineage + of the original work without explicit permission from Contributors. + +3.3. Ethical Use Declaration + Commercial use of Covered Software requires acknowledgment that You have + read and understood Exhibit A (Ethical Use Guidelines) and agree to act + in good faith accordance with its principles. + +See Exhibit A for complete Ethical Use Guidelines. + +-------------------------------------------------------------------------------- +SECTION 4: PROVENANCE REQUIREMENTS +-------------------------------------------------------------------------------- + +4.1. Metadata Preservation + You must not strip, alter, or obscure Provenance Metadata from Covered + Software except where technically necessary and with clear documentation + of any changes. + +4.2. Quantum-Safe Provenance (Optional) + Contributors may sign their Contributions using Quantum-Safe Signatures. + If Quantum-Safe Signatures are present, You must preserve them in all + distributions. + +4.3. Lineage Chain + When creating derivative works, You should extend the provenance chain + to include Your own contributions, maintaining cryptographic linkage to + prior Contributors where feasible. + +See Exhibit B for Quantum-Safe Provenance specifications. + +-------------------------------------------------------------------------------- +SECTION 5: GOVERNANCE +-------------------------------------------------------------------------------- + +5.1. Stewardship Council + This License is maintained by the Palimpsest Stewardship Council, which + may issue clarifications, interpretive guidance, and future versions. + +5.2. Version Selection + You may use Covered Software under this version of the License or any + later version published by the Palimpsest Stewardship Council. + +5.3. Dispute Resolution + Disputes regarding interpretation of Ethical Use Requirements (Section 3) + should first be submitted to the Palimpsest Stewardship Council for + non-binding guidance before pursuing legal remedies. + +-------------------------------------------------------------------------------- +SECTION 6: COMPATIBILITY +-------------------------------------------------------------------------------- + +6.1. MPL-2.0 Compatibility + Covered Software under this License may be combined with software under + MPL-2.0. The combined work must comply with both licenses. + +6.2. Secondary Licenses + The Secondary License provisions of MPL-2.0 Section 3.3 apply to this + License. + +-------------------------------------------------------------------------------- +EXHIBITS +-------------------------------------------------------------------------------- + +Exhibit A - Ethical Use Guidelines +Exhibit B - Quantum-Safe Provenance Specification + +See separate files: +- EXHIBIT-A-ETHICAL-USE.txt +- EXHIBIT-B-QUANTUM-SAFE.txt + +-------------------------------------------------------------------------------- +END OF PALIMPSEST-MPL LICENSE VERSION 1.0 +-------------------------------------------------------------------------------- + +For questions about this License: +- Repository: https://github.com/hyperpolymath/palimpsest-license +- Council: contact via repository Issues diff --git a/MOVED.md b/MOVED.md new file mode 100644 index 0000000..d73fd16 --- /dev/null +++ b/MOVED.md @@ -0,0 +1,58 @@ +# This Repository Has Moved + +**GQLdt is now part of the [Lithoglyph monorepo](https://github.com/hyperpolymath/lith).** + +## New Location + +- **Monorepo:** https://github.com/hyperpolymath/lith +- **Query Language:** https://github.com/hyperpolymath/lith/tree/main/query + +## Why the Move? + +GQLdt (Lithoglyph Query Language with dependent types) is the query interface for Lithoglyph. To improve discoverability and maintenance, we've consolidated the Lithoglyph ecosystem into a single monorepo: + +``` +lith/ +├── query/ # GQLdt (this repo) +├── database/ # Form.Model + Form.Blocks (Forth core) +├── bridge/ # Zig FFI bridge +├── studio/ # Web-based GUI +└── debugger/ # Proof-carrying debugger +``` + +## Benefits of the Monorepo + +- **Single source of truth** for all Lithoglyph components +- **Coordinated versioning** across query language, database, and tools +- **Unified documentation** and examples +- **Shared CI/CD** and dependency management +- **Easier cross-component refactoring** + +## Migration Guide + +### For Users + +Update your imports/dependencies: + +**Before:** +```bash +git clone https://github.com/hyperpolymath/gql-dt +``` + +**After:** +```bash +git clone https://github.com/hyperpolymath/lith +cd lith/query +``` + +### For Contributors + +Submit PRs to the [lith monorepo](https://github.com/hyperpolymath/lith) instead. + +## This Repository's Future + +This repository (`gql-dt`) will be archived and remain as a historical reference. All active development happens in the monorepo. + +--- + +**See you at [github.com/hyperpolymath/lith](https://github.com/hyperpolymath/lith)!** 🚀 diff --git a/bridge/zig/build.zig b/bridge/zig/build.zig new file mode 100644 index 0000000..f749e1f --- /dev/null +++ b/bridge/zig/build.zig @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Zig FFI Bridge for GQLdt +// Provides C-compatible ABI for Lean 4 integration + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Static library for FFI bridge + const lib = b.addStaticLibrary(.{ + .name = "lith_bridge", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // Export C ABI + lib.linkLibC(); + + b.installArtifact(lib); + + // Tests + const main_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); + + // Integration tests + const integration_tests = b.addTest(.{ + .root_source_file = b.path("test/integration_test.zig"), + .target = target, + .optimize = optimize, + }); + + integration_tests.linkLibrary(lib); + + const run_integration_tests = b.addRunArtifact(integration_tests); + + const integration_step = b.step("test-integration", "Run integration tests"); + integration_step.dependOn(&run_integration_tests.step); +} diff --git a/bridge/zig/src/main.zig b/bridge/zig/src/main.zig new file mode 100644 index 0000000..a95c91c --- /dev/null +++ b/bridge/zig/src/main.zig @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (@hyperpolymath) +// +// Zig FFI Bridge - Main Module +// Bidirectional FFI: Lean 4 ↔ Zig ↔ Lith Forth core + +const std = @import("std"); + +/// Status code for FFI operations +pub const LithStatus = enum(i32) { + ok = 0, + error_null_pointer = 1, + error_invalid_proof = 2, + error_type_mismatch = 3, + error_constraint_violation = 4, + error_out_of_memory = 5, + + pub fn toC(self: LithStatus) c_int { + return @intFromEnum(self); + } +}; + +/// Opaque database handle (non-null guaranteed by Lean 4 types) +pub const LithDb = opaque {}; + +/// FFI-safe string view (non-owning) +pub const LithString = struct { + data: [*]const u8, + len: usize, + + pub fn fromSlice(slice: []const u8) LithString { + return .{ .data = slice.ptr, .len = slice.len }; + } + + pub fn toSlice(self: LithString) []const u8 { + return self.data[0..self.len]; + } +}; + +/// Forward: Lean 4 → Zig → Lith +/// Insert operation with proof blob +export fn lith_insert( + db: *LithDb, + collection: [*:0]const u8, + document: [*]const u8, + doc_len: usize, + proof_blob: [*]const u8, + proof_len: usize, +) callconv(.C) c_int { + _ = db; + _ = collection; + _ = document; + _ = doc_len; + _ = proof_blob; + _ = proof_len; + + // TODO: Implement actual insertion + // 1. Deserialize proof blob (CBOR) + // 2. Verify proof against schema + // 3. Insert into Lith via Forth FFI + // 4. Return status + + return LithStatus.ok.toC(); +} + +/// Reverse: Lith → Zig → Lean 4 +/// Register constraint checker callback +export fn lith_register_constraint_checker( + db: *LithDb, + checker: *const fn (doc: [*]const u8, len: usize) callconv(.C) bool, +) callconv(.C) c_int { + _ = db; + _ = checker; + + // TODO: Implement callback registration + // Store function pointer for later invocation + // When Lith validates data, call this Lean 4 checker + + return LithStatus.ok.toC(); +} + +/// Get discovered functional dependencies +export fn lith_get_discovered_fds( + db: *LithDb, + collection: [*:0]const u8, + out_fds: *[*]u8, + out_len: *usize, +) callconv(.C) c_int { + _ = db; + _ = collection; + _ = out_fds; + _ = out_len; + + // TODO: Implement FD discovery + // 1. Query Lith for collection statistics + // 2. Run FD discovery algorithm (DFD, TANE, etc.) + // 3. Serialize FDs to CBOR + // 4. Return pointer + length + + return LithStatus.ok.toC(); +} + +/// Verify normalization proof +export fn lith_verify_normalization_proof( + db: *LithDb, + step_blob: [*]const u8, + step_len: usize, + proof_blob: [*]const u8, + proof_len: usize, +) callconv(.C) c_int { + _ = db; + _ = step_blob; + _ = step_len; + _ = proof_blob; + _ = proof_len; + + // TODO: Implement proof verification + // 1. Deserialize normalization step + // 2. Deserialize Lean 4 proof + // 3. Verify proof is valid for step + // 4. Return status + + return LithStatus.ok.toC(); +} + +/// Free memory allocated by FFI functions +export fn lith_free(ptr: [*]u8, len: usize) callconv(.C) void { + const allocator = std.heap.c_allocator; + const slice = ptr[0..len]; + allocator.free(slice); +} + +test "LithStatus roundtrip" { + const status = LithStatus.ok; + try std.testing.expectEqual(@as(c_int, 0), status.toC()); +} + +test "LithString conversion" { + const str = "Hello, Lith!"; + const lith_str = LithString.fromSlice(str); + try std.testing.expectEqualSlices(u8, str, lith_str.toSlice()); +} diff --git a/bridge/zig/test/integration_test.zig b/bridge/zig/test/integration_test.zig new file mode 100644 index 0000000..9a8f36b --- /dev/null +++ b/bridge/zig/test/integration_test.zig @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Integration tests for Zig FFI bridge + +const std = @import("std"); +const main = @import("main"); + +test "lith_insert stub returns ok" { + // Mock database handle (in production, would be created by Lith) + var db: main.LithDb = undefined; + const db_ptr = @as(*main.LithDb, @ptrCast(&db)); + + const collection = "test_collection"; + const document = "{\"id\": 1, \"value\": 42}"; + const proof = "{}"; // Empty proof for stub + + const status = main.lith_insert( + db_ptr, + collection, + document.ptr, + document.len, + proof.ptr, + proof.len, + ); + + try std.testing.expectEqual(@as(c_int, 0), status); +} + +test "lith_register_constraint_checker stub" { + var db: main.LithDb = undefined; + const db_ptr = @as(*main.LithDb, @ptrCast(&db)); + + const checker = struct { + fn check(doc: [*]const u8, len: usize) callconv(.C) bool { + _ = doc; + _ = len; + return true; + } + }.check; + + const status = main.lith_register_constraint_checker(db_ptr, checker); + + try std.testing.expectEqual(@as(c_int, 0), status); +} diff --git a/ffi/zig/build.zig b/ffi/zig/build.zig new file mode 100644 index 0000000..0961651 --- /dev/null +++ b/ffi/zig/build.zig @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell (@hyperpolymath) +// +// build.zig - GQL-DT FFI Build Configuration (Zig 0.15.2+) +// +// Builds the libgqldt shared library (C ABI) and unit tests. + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Static library (libgqldt.a) + const static_lib = b.addLibrary(.{ + .name = "gqldt", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + .linkage = .static, + }); + b.installArtifact(static_lib); + + // Shared library (libgqldt.so / libgqldt.dylib) + const shared_lib = b.addLibrary(.{ + .name = "gqldt", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + .linkage = .dynamic, + }); + b.installArtifact(shared_lib); + + // Unit tests (from main.zig internal tests) + const unit_tests = b.addTest(.{ + .name = "gqldt-tests", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/ffi/zig/src/main.zig b/ffi/zig/src/main.zig new file mode 100644 index 0000000..e3ee8af --- /dev/null +++ b/ffi/zig/src/main.zig @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell (@hyperpolymath) +// +// main.zig - Zig FFI implementation for GQL-DT ABI +// +// Pure ABI bridge — all safety logic in Idris2. +// Delegates database operations to the Lith bridge layer (lith_persist/lith_insert). +// Manages opaque handle lifecycle and query parsing at the FFI boundary. + +const std = @import("std"); +const testing = std.testing; + +// --------------------------------------------------------------------------- +// Status codes (matches Idris2 GqldtStatus in GQLdt.ABI.Types) +// --------------------------------------------------------------------------- + +/// Result status codes for GQL-DT FFI operations. +/// Integer values match the Idris2 statusToInt mapping exactly. +pub const Status = enum(i32) { + ok = 0, + invalid_arg = 1, + type_mismatch = 2, + proof_failed = 3, + permission_denied = 4, + out_of_memory = 5, + internal_error = 6, +}; + +// --------------------------------------------------------------------------- +// Handle backing structures +// --------------------------------------------------------------------------- + +/// Internal database state backing the opaque GqldtDb handle. +/// Stores the path used to open the database and tracks open state. +const DbState = struct { + path_buf: [4096]u8 = undefined, + path_len: usize = 0, + opened: bool = false, +}; + +/// Internal query state backing the opaque GqldtQuery handle. +/// Stores the raw query text and whether it has been type-checked. +const QueryState = struct { + text_buf: [1_000_000]u8 = undefined, + text_len: usize = 0, + type_checked: bool = false, + inferred: bool = false, +}; + +/// Internal schema state backing the opaque GqldtSchema handle. +/// Stores the collection name the schema was retrieved for. +const SchemaState = struct { + collection_buf: [256]u8 = undefined, + collection_len: usize = 0, +}; + +/// Internal result-set state backing the opaque GqldtResult handle. +/// Stores serialised query output after execution. +const ResultState = struct { + data_buf: [1_000_000]u8 = undefined, + data_len: usize = 0, +}; + +// --------------------------------------------------------------------------- +// Opaque handle types (matches Idris2 handle types) +// --------------------------------------------------------------------------- + +pub const GqldtDb = opaque {}; +pub const GqldtQuery = opaque {}; +pub const GqldtSchema = opaque {}; +pub const GqldtType = opaque {}; +pub const GqldtResult = opaque {}; + +// --------------------------------------------------------------------------- +// Global library state +// --------------------------------------------------------------------------- + +/// Whether gqldt_init() has been called successfully. +var initialized: bool = false; + +/// General-purpose allocator for handle allocations. +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +// --------------------------------------------------------------------------- +// Library Lifecycle +// --------------------------------------------------------------------------- + +/// Initialise the GQL-DT library. +/// Idempotent — repeated calls return ok without side-effects. +export fn gqldt_init() callconv(.c) i32 { + if (initialized) return @intFromEnum(Status.ok); + initialized = true; + return @intFromEnum(Status.ok); +} + +/// Tear down the GQL-DT library and release the backing allocator. +export fn gqldt_cleanup() callconv(.c) void { + if (!initialized) return; + _ = gpa.deinit(); + gpa = std.heap.GeneralPurposeAllocator(.{}){}; + initialized = false; +} + +// --------------------------------------------------------------------------- +// Database Operations +// --------------------------------------------------------------------------- + +/// Open a database at the given path and write an opaque handle into *db_out. +/// +/// Path validation is expected to happen on the Idris2 side via Proven.SafePath +/// before this function is called. The FFI layer performs basic sanity checks +/// (non-null, bounded length) and allocates a DbState on the heap. +export fn gqldt_db_open( + path: [*:0]const u8, + path_len: u64, + db_out: *?*GqldtDb, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (path_len == 0) return @intFromEnum(Status.invalid_arg); + if (path_len > 4096) return @intFromEnum(Status.invalid_arg); + + const len: usize = @intCast(path_len); + + // Validate that the path is a reasonable C string. + if (!validate_c_string(path, len)) return @intFromEnum(Status.invalid_arg); + + const allocator = gpa.allocator(); + const state = allocator.create(DbState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + @memcpy(state.path_buf[0..len], path[0..len]); + state.path_len = len; + state.opened = true; + + // SAFETY: DbState is a heap-allocated struct; casting its pointer to the + // opaque GqldtDb handle is the standard pattern for Zig FFI bridges. + db_out.* = @ptrCast(state); + return @intFromEnum(Status.ok); +} + +/// Close a previously-opened database handle and free its backing memory. +export fn gqldt_db_close(db: *GqldtDb) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + + // SAFETY: The caller must pass a handle originally obtained from + // gqldt_db_open. We cast back to the concrete DbState to free it. + const state: *DbState = @ptrCast(@alignCast(db)); + state.opened = false; + + const allocator = gpa.allocator(); + allocator.destroy(state); + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Query Parsing and Type Checking +// --------------------------------------------------------------------------- + +/// Parse a GQL-DT query string (explicit dependent types). +/// +/// String validation is expected to happen on the Idris2 side via +/// Proven.SafeString before this function is called. The FFI layer stores +/// the raw query text in a heap-allocated QueryState. +export fn gqldt_parse( + query_str: [*:0]const u8, + query_len: u64, + query_out: *?*GqldtQuery, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (query_len == 0) return @intFromEnum(Status.invalid_arg); + if (query_len > 1_000_000) return @intFromEnum(Status.invalid_arg); + + const len: usize = @intCast(query_len); + + if (!validate_c_string(query_str, len)) return @intFromEnum(Status.invalid_arg); + + const allocator = gpa.allocator(); + const state = allocator.create(QueryState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + @memcpy(state.text_buf[0..len], query_str[0..len]); + state.text_len = len; + state.type_checked = false; + state.inferred = false; + + // SAFETY: Heap-allocated QueryState cast to opaque handle. + query_out.* = @ptrCast(state); + return @intFromEnum(Status.ok); +} + +/// Parse a GQL query string with type inference against a schema. +/// +/// Type inference is delegated to the Idris2 layer; at the FFI level we +/// record that this query was parsed with inference enabled. +export fn gqldt_parse_inferred( + query_str: [*:0]const u8, + query_len: u64, + schema: *GqldtSchema, + query_out: *?*GqldtQuery, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (query_len == 0) return @intFromEnum(Status.invalid_arg); + if (query_len > 1_000_000) return @intFromEnum(Status.invalid_arg); + + const len: usize = @intCast(query_len); + + if (!validate_c_string(query_str, len)) return @intFromEnum(Status.invalid_arg); + + // Validate the schema handle is non-null (opaque pointer check). + _ = schema; + + const allocator = gpa.allocator(); + const state = allocator.create(QueryState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + @memcpy(state.text_buf[0..len], query_str[0..len]); + state.text_len = len; + state.type_checked = false; + state.inferred = true; + + // SAFETY: Heap-allocated QueryState cast to opaque handle. + query_out.* = @ptrCast(state); + return @intFromEnum(Status.ok); +} + +/// Type-check a parsed query against a schema. +/// +/// Full dependent-type checking happens in Idris2. The FFI layer marks the +/// query as type-checked so that gqldt_execute can verify the precondition. +export fn gqldt_typecheck( + query: *GqldtQuery, + schema: *GqldtSchema, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + + _ = schema; + + // SAFETY: Caller must pass a handle from gqldt_parse / gqldt_parse_inferred. + const state: *QueryState = @ptrCast(@alignCast(query)); + if (state.text_len == 0) return @intFromEnum(Status.invalid_arg); + + state.type_checked = true; + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Query Execution +// --------------------------------------------------------------------------- + +/// Execute a type-checked query against an open database. +/// +/// Execution with provenance tracking is handled by the Idris2 layer. The +/// FFI layer allocates a ResultState to hold the output and verifies that the +/// query has been type-checked beforehand. +export fn gqldt_execute( + db: *GqldtDb, + query: *GqldtQuery, + result_out: *?*GqldtResult, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + + // SAFETY: Handles must originate from their respective open/parse calls. + const db_state: *DbState = @ptrCast(@alignCast(db)); + const q_state: *QueryState = @ptrCast(@alignCast(query)); + + if (!db_state.opened) return @intFromEnum(Status.internal_error); + if (q_state.text_len == 0) return @intFromEnum(Status.invalid_arg); + if (!q_state.type_checked) return @intFromEnum(Status.type_mismatch); + + const allocator = gpa.allocator(); + const r_state = allocator.create(ResultState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + // Placeholder result — in production the Idris2 execute function fills + // this with CBOR-encoded result rows. + r_state.data_len = 0; + + // SAFETY: Heap-allocated ResultState cast to opaque handle. + result_out.* = @ptrCast(r_state); + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +/// Serialise a parsed query to CBOR (RFC 8949). +/// +/// Semantic-tag encoding is performed in Idris2 via the CborTag definitions. +/// The FFI layer writes a minimal CBOR text-string encoding of the raw query +/// into the caller-provided buffer. +export fn gqldt_serialize_cbor( + query: *GqldtQuery, + buffer: [*]u8, + buffer_len: u64, + written_out: *u64, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (buffer_len == 0) return @intFromEnum(Status.invalid_arg); + + // SAFETY: Handle from gqldt_parse. + const state: *QueryState = @ptrCast(@alignCast(query)); + if (state.text_len == 0) return @intFromEnum(Status.invalid_arg); + + const buf_len: usize = @intCast(buffer_len); + + // Encode as CBOR text string: major type 3 + length prefix + UTF-8 bytes. + // This is a minimal encoding; full semantic tagging is handled by Idris2. + const header_size = cbor_text_header_size(state.text_len); + const total = header_size + state.text_len; + + if (total > buf_len) return @intFromEnum(Status.invalid_arg); + + var pos: usize = 0; + write_cbor_text_header(buffer, &pos, state.text_len); + @memcpy(buffer[pos .. pos + state.text_len], state.text_buf[0..state.text_len]); + pos += state.text_len; + + written_out.* = @intCast(pos); + return @intFromEnum(Status.ok); +} + +/// Serialise a parsed query to JSON. +/// +/// Produces a JSON object wrapping the raw query text. +export fn gqldt_serialize_json( + query: *GqldtQuery, + buffer: [*]u8, + buffer_len: u64, + written_out: *u64, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (buffer_len == 0) return @intFromEnum(Status.invalid_arg); + + // SAFETY: Handle from gqldt_parse. + const state: *QueryState = @ptrCast(@alignCast(query)); + if (state.text_len == 0) return @intFromEnum(Status.invalid_arg); + + const buf_len: usize = @intCast(buffer_len); + + // Produce: {"query":""} + const prefix = "{\"query\":\""; + const suffix = "\"}"; + const total = prefix.len + state.text_len + suffix.len; + + if (total > buf_len) return @intFromEnum(Status.invalid_arg); + + var pos: usize = 0; + @memcpy(buffer[pos .. pos + prefix.len], prefix); + pos += prefix.len; + @memcpy(buffer[pos .. pos + state.text_len], state.text_buf[0..state.text_len]); + pos += state.text_len; + @memcpy(buffer[pos .. pos + suffix.len], suffix); + pos += suffix.len; + + written_out.* = @intCast(pos); + return @intFromEnum(Status.ok); +} + +/// Deserialise a query from CBOR. +/// +/// Expects a CBOR text-string encoding (major type 3). Full semantic-tag +/// validation is handled in Idris2. +export fn gqldt_deserialize_cbor( + buffer: [*]const u8, + buffer_len: u64, + query_out: *?*GqldtQuery, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + if (buffer_len == 0) return @intFromEnum(Status.invalid_arg); + if (buffer_len > 10_000_000) return @intFromEnum(Status.invalid_arg); + + const buf_len: usize = @intCast(buffer_len); + + // Decode CBOR text string header (major type 3). + var pos: usize = 0; + const text_len = read_cbor_text_header(buffer, buf_len, &pos) orelse { + return @intFromEnum(Status.invalid_arg); + }; + + if (pos + text_len > buf_len) return @intFromEnum(Status.invalid_arg); + if (text_len == 0) return @intFromEnum(Status.invalid_arg); + if (text_len > 1_000_000) return @intFromEnum(Status.invalid_arg); + + const allocator = gpa.allocator(); + const state = allocator.create(QueryState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + @memcpy(state.text_buf[0..text_len], buffer[pos .. pos + text_len]); + state.text_len = text_len; + state.type_checked = false; + state.inferred = false; + + // SAFETY: Heap-allocated QueryState cast to opaque handle. + query_out.* = @ptrCast(state); + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Schema Operations +// --------------------------------------------------------------------------- + +/// Retrieve the schema for a named collection from an open database. +/// +/// Schema extraction and validation are performed in Idris2. The FFI layer +/// allocates a SchemaState that records the collection name. +export fn gqldt_get_schema( + db: *GqldtDb, + collection_name: [*:0]const u8, + schema_out: *?*GqldtSchema, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + + // SAFETY: Handle from gqldt_db_open. + const db_state: *DbState = @ptrCast(@alignCast(db)); + if (!db_state.opened) return @intFromEnum(Status.internal_error); + + // Measure collection name length (null-terminated). + var name_len: usize = 0; + while (collection_name[name_len] != 0) : (name_len += 1) { + if (name_len >= 256) return @intFromEnum(Status.invalid_arg); + } + if (name_len == 0) return @intFromEnum(Status.invalid_arg); + + const allocator = gpa.allocator(); + const state = allocator.create(SchemaState) catch { + return @intFromEnum(Status.out_of_memory); + }; + + @memcpy(state.collection_buf[0..name_len], collection_name[0..name_len]); + state.collection_len = name_len; + + // SAFETY: Heap-allocated SchemaState cast to opaque handle. + schema_out.* = @ptrCast(state); + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Permission Validation +// --------------------------------------------------------------------------- + +/// Validate query permissions using the two-tier TypeWhitelist system. +/// +/// Permission checking with TypeWhitelist is performed in Idris2. The FFI +/// layer delegates the decision and returns the result. Currently returns +/// ok for all queries — real enforcement is in the Idris2 layer. +export fn gqldt_validate_permissions( + query: *GqldtQuery, + user_id: [*:0]const u8, + permissions: *const anyopaque, +) callconv(.c) i32 { + if (!initialized) return @intFromEnum(Status.internal_error); + + // SAFETY: Handle from gqldt_parse. + const q_state: *QueryState = @ptrCast(@alignCast(query)); + if (q_state.text_len == 0) return @intFromEnum(Status.invalid_arg); + + // Validate user_id is non-empty. + if (user_id[0] == 0) return @intFromEnum(Status.invalid_arg); + + // Permissions opaque pointer must be non-null (checked by C ABI contract). + _ = permissions; + + // Permission enforcement happens in Idris2 — FFI layer grants by default. + return @intFromEnum(Status.ok); +} + +// --------------------------------------------------------------------------- +// Resource Cleanup +// --------------------------------------------------------------------------- + +/// Free a query handle previously returned by gqldt_parse, +/// gqldt_parse_inferred, or gqldt_deserialize_cbor. +export fn gqldt_query_free(query: *GqldtQuery) callconv(.c) void { + if (!initialized) return; + + // SAFETY: Handle from gqldt_parse / gqldt_parse_inferred / gqldt_deserialize_cbor. + const state: *QueryState = @ptrCast(@alignCast(query)); + const allocator = gpa.allocator(); + allocator.destroy(state); +} + +/// Free a schema handle previously returned by gqldt_get_schema. +export fn gqldt_schema_free(schema: *GqldtSchema) callconv(.c) void { + if (!initialized) return; + + // SAFETY: Handle from gqldt_get_schema. + const state: *SchemaState = @ptrCast(@alignCast(schema)); + const allocator = gpa.allocator(); + allocator.destroy(state); +} + +/// Free a result handle previously returned by gqldt_execute. +export fn gqldt_result_free(result: *GqldtResult) callconv(.c) void { + if (!initialized) return; + + // SAFETY: Handle from gqldt_execute. + const state: *ResultState = @ptrCast(@alignCast(result)); + const allocator = gpa.allocator(); + allocator.destroy(state); +} + +// --------------------------------------------------------------------------- +// Version Information +// --------------------------------------------------------------------------- + +/// Return the GQL-DT FFI library version as a null-terminated C string. +export fn gqldt_version() callconv(.c) [*:0]const u8 { + return "0.1.0"; +} + +// --------------------------------------------------------------------------- +// Slot Allocation Helpers (for Idris2 prim__allocSlot / prim__readSlot) +// --------------------------------------------------------------------------- + +/// Allocate a pointer-sized output slot on the heap. +/// Used by Idris2 to receive opaque handle pointers from FFI calls. +export fn gqldt_alloc_slot() callconv(.c) ?*anyopaque { + const allocator = gpa.allocator(); + const slot = allocator.create(usize) catch return null; + slot.* = 0; + // SAFETY: usize* cast to opaque — Idris2 treats it as AnyPtr. + return @ptrCast(slot); +} + +/// Read a pointer value from an output slot as u64 (Bits64 in Idris2). +export fn gqldt_read_slot(slot: *anyopaque) callconv(.c) u64 { + // SAFETY: Slot must have been allocated by gqldt_alloc_slot. + const typed: *usize = @ptrCast(@alignCast(slot)); + return @intCast(typed.*); +} + +/// Free an output slot allocated by gqldt_alloc_slot. +export fn gqldt_free_slot(slot: *anyopaque) callconv(.c) void { + // SAFETY: Slot must have been allocated by gqldt_alloc_slot. + const typed: *usize = @ptrCast(@alignCast(slot)); + const allocator = gpa.allocator(); + allocator.destroy(typed); +} + +/// Cast a Bits64 value to an opaque pointer. +/// Used by Idris2 to convert handle values back to pointers for FFI calls. +export fn gqldt_bits64_to_ptr(value: u64) callconv(.c) ?*anyopaque { + if (value == 0) return null; + // SAFETY: The caller guarantees that value was originally obtained from + // a valid pointer via gqldt_read_slot. + return @ptrFromInt(@as(usize, @intCast(value))); +} + +// --------------------------------------------------------------------------- +// Helper Functions (ABI Bridge Only) +// --------------------------------------------------------------------------- + +/// Validate a null-terminated C string has non-zero length and fits within +/// max_len bytes. +/// +/// NOTE: Full validation happens in Idris2 via Proven.SafeString. +fn validate_c_string(ptr: [*:0]const u8, max_len: usize) bool { + var len: usize = 0; + while (ptr[len] != 0) : (len += 1) { + if (len >= max_len) return false; + } + return len > 0; +} + +/// Compute the number of header bytes needed for a CBOR text string of the +/// given length (major type 3). +fn cbor_text_header_size(text_len: usize) usize { + if (text_len < 24) return 1; + if (text_len <= 0xFF) return 2; + if (text_len <= 0xFFFF) return 3; + if (text_len <= 0xFFFFFFFF) return 5; + return 9; +} + +/// Write a CBOR text string header (major type 3) into buffer at *pos. +fn write_cbor_text_header(buffer: [*]u8, pos: *usize, text_len: usize) void { + const major: u8 = 3 << 5; // major type 3 + if (text_len < 24) { + buffer[pos.*] = major | @as(u8, @truncate(text_len)); + pos.* += 1; + } else if (text_len <= 0xFF) { + buffer[pos.*] = major | 24; + pos.* += 1; + buffer[pos.*] = @truncate(text_len); + pos.* += 1; + } else if (text_len <= 0xFFFF) { + buffer[pos.*] = major | 25; + pos.* += 1; + const be = std.mem.nativeToBig(u16, @truncate(text_len)); + const bytes = std.mem.toBytes(be); + @memcpy(buffer[pos.* .. pos.* + 2], &bytes); + pos.* += 2; + } else if (text_len <= 0xFFFFFFFF) { + buffer[pos.*] = major | 26; + pos.* += 1; + const be = std.mem.nativeToBig(u32, @truncate(text_len)); + const bytes = std.mem.toBytes(be); + @memcpy(buffer[pos.* .. pos.* + 4], &bytes); + pos.* += 4; + } else { + buffer[pos.*] = major | 27; + pos.* += 1; + const be = std.mem.nativeToBig(u64, @as(u64, text_len)); + const bytes = std.mem.toBytes(be); + @memcpy(buffer[pos.* .. pos.* + 8], &bytes); + pos.* += 8; + } +} + +/// Read a CBOR text string header (major type 3) from buffer, advancing *pos. +/// Returns null if the header is invalid or not a text string. +fn read_cbor_text_header(buffer: [*]const u8, buf_len: usize, pos: *usize) ?usize { + if (pos.* >= buf_len) return null; + + const initial = buffer[pos.*]; + const major = initial >> 5; + if (major != 3) return null; // Not a text string. + + const additional = initial & 0x1F; + pos.* += 1; + + if (additional < 24) { + return @as(usize, additional); + } else if (additional == 24) { + if (pos.* >= buf_len) return null; + const len: usize = buffer[pos.*]; + pos.* += 1; + return len; + } else if (additional == 25) { + if (pos.* + 2 > buf_len) return null; + var bytes: [2]u8 = undefined; + @memcpy(&bytes, buffer[pos.* .. pos.* + 2]); + pos.* += 2; + return @as(usize, std.mem.bigToNative(u16, @bitCast(bytes))); + } else if (additional == 26) { + if (pos.* + 4 > buf_len) return null; + var bytes: [4]u8 = undefined; + @memcpy(&bytes, buffer[pos.* .. pos.* + 4]); + pos.* += 4; + return @as(usize, std.mem.bigToNative(u32, @bitCast(bytes))); + } else if (additional == 27) { + if (pos.* + 8 > buf_len) return null; + var bytes: [8]u8 = undefined; + @memcpy(&bytes, buffer[pos.* .. pos.* + 8]); + pos.* += 8; + return @as(usize, std.mem.bigToNative(u64, @bitCast(bytes))); + } + + return null; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test "status codes match Idris2 ABI" { + try testing.expectEqual(@as(i32, 0), @intFromEnum(Status.ok)); + try testing.expectEqual(@as(i32, 1), @intFromEnum(Status.invalid_arg)); + try testing.expectEqual(@as(i32, 2), @intFromEnum(Status.type_mismatch)); + try testing.expectEqual(@as(i32, 3), @intFromEnum(Status.proof_failed)); + try testing.expectEqual(@as(i32, 4), @intFromEnum(Status.permission_denied)); + try testing.expectEqual(@as(i32, 5), @intFromEnum(Status.out_of_memory)); + try testing.expectEqual(@as(i32, 6), @intFromEnum(Status.internal_error)); +} + +test "library initialization" { + const status = gqldt_init(); + try testing.expectEqual(@intFromEnum(Status.ok), status); + gqldt_cleanup(); +} + +test "library double-init is idempotent" { + _ = gqldt_init(); + const status = gqldt_init(); + try testing.expectEqual(@intFromEnum(Status.ok), status); + gqldt_cleanup(); +} + +test "validate_c_string rejects empty strings" { + const empty_str: [*:0]const u8 = ""; + try testing.expect(!validate_c_string(empty_str, 100)); +} + +test "validate_c_string accepts valid strings" { + const valid_str: [*:0]const u8 = "SELECT * FROM users"; + try testing.expect(validate_c_string(valid_str, 1000)); +} + +test "validate_c_string rejects oversized strings" { + var buf: [200]u8 = undefined; + @memset(&buf, 'A'); + buf[199] = 0; + const long_str: [*:0]const u8 = @ptrCast(&buf); + try testing.expect(!validate_c_string(long_str, 100)); +} + +test "db_open and db_close round-trip" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*GqldtDb = null; + const open_status = gqldt_db_open("test.db", 7, &db); + try testing.expectEqual(@intFromEnum(Status.ok), open_status); + try testing.expect(db != null); + + const close_status = gqldt_db_close(db.?); + try testing.expectEqual(@intFromEnum(Status.ok), close_status); +} + +test "db_open rejects empty path" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*GqldtDb = null; + const status = gqldt_db_open("", 0, &db); + try testing.expectEqual(@intFromEnum(Status.invalid_arg), status); +} + +test "parse and query_free round-trip" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*GqldtQuery = null; + const query_text: [*:0]const u8 = "MATCH (n:Person) RETURN n"; + const status = gqldt_parse(query_text, 25, &query); + try testing.expectEqual(@intFromEnum(Status.ok), status); + try testing.expect(query != null); + + gqldt_query_free(query.?); +} + +test "parse rejects empty query" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*GqldtQuery = null; + const status = gqldt_parse("", 0, &query); + try testing.expectEqual(@intFromEnum(Status.invalid_arg), status); +} + +test "typecheck marks query as checked" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + // Create a schema handle. + var db: ?*GqldtDb = null; + _ = gqldt_db_open("test.db", 7, &db); + defer _ = gqldt_db_close(db.?); + + var schema: ?*GqldtSchema = null; + _ = gqldt_get_schema(db.?, "users", &schema); + defer gqldt_schema_free(schema.?); + + // Parse a query. + var query: ?*GqldtQuery = null; + _ = gqldt_parse("MATCH (n) RETURN n", 18, &query); + defer gqldt_query_free(query.?); + + // Type-check. + const tc_status = gqldt_typecheck(query.?, schema.?); + try testing.expectEqual(@intFromEnum(Status.ok), tc_status); +} + +test "execute requires type-checked query" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*GqldtDb = null; + _ = gqldt_db_open("test.db", 7, &db); + defer _ = gqldt_db_close(db.?); + + var query: ?*GqldtQuery = null; + _ = gqldt_parse("MATCH (n) RETURN n", 18, &query); + defer gqldt_query_free(query.?); + + // Attempt execution without type-checking — should fail. + var result: ?*GqldtResult = null; + const status = gqldt_execute(db.?, query.?, &result); + try testing.expectEqual(@intFromEnum(Status.type_mismatch), status); +} + +test "execute succeeds after typecheck" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*GqldtDb = null; + _ = gqldt_db_open("test.db", 7, &db); + defer _ = gqldt_db_close(db.?); + + var schema: ?*GqldtSchema = null; + _ = gqldt_get_schema(db.?, "users", &schema); + defer gqldt_schema_free(schema.?); + + var query: ?*GqldtQuery = null; + _ = gqldt_parse("MATCH (n) RETURN n", 18, &query); + defer gqldt_query_free(query.?); + + _ = gqldt_typecheck(query.?, schema.?); + + var result: ?*GqldtResult = null; + const exec_status = gqldt_execute(db.?, query.?, &result); + try testing.expectEqual(@intFromEnum(Status.ok), exec_status); + try testing.expect(result != null); + + gqldt_result_free(result.?); +} + +test "serialize_cbor and deserialize_cbor round-trip" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + const query_text: [*:0]const u8 = "MATCH (n) RETURN n"; + var query: ?*GqldtQuery = null; + _ = gqldt_parse(query_text, 18, &query); + defer gqldt_query_free(query.?); + + // Serialise to CBOR. + var cbor_buf: [1024]u8 = undefined; + var written: u64 = 0; + const ser_status = gqldt_serialize_cbor(query.?, &cbor_buf, 1024, &written); + try testing.expectEqual(@intFromEnum(Status.ok), ser_status); + try testing.expect(written > 0); + + // Deserialise from CBOR. + var query2: ?*GqldtQuery = null; + const deser_status = gqldt_deserialize_cbor(&cbor_buf, written, &query2); + try testing.expectEqual(@intFromEnum(Status.ok), deser_status); + try testing.expect(query2 != null); + + gqldt_query_free(query2.?); +} + +test "serialize_json produces valid wrapper" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*GqldtQuery = null; + _ = gqldt_parse("RETURN 1", 8, &query); + defer gqldt_query_free(query.?); + + var json_buf: [1024]u8 = undefined; + var written: u64 = 0; + const status = gqldt_serialize_json(query.?, &json_buf, 1024, &written); + try testing.expectEqual(@intFromEnum(Status.ok), status); + + const json_str = json_buf[0..@intCast(written)]; + try testing.expectEqualStrings("{\"query\":\"RETURN 1\"}", json_str); +} + +test "version returns semantic version" { + const ver = gqldt_version(); + const ver_str = std.mem.span(ver); + try testing.expect(ver_str.len > 0); + try testing.expect(std.mem.count(u8, ver_str, ".") >= 1); +} diff --git a/ffi/zig/test/integration_test.zig b/ffi/zig/test/integration_test.zig new file mode 100644 index 0000000..8fc1d46 --- /dev/null +++ b/ffi/zig/test/integration_test.zig @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell (@hyperpolymath) +// +// GQL-DT Integration Tests +// +// These tests verify that the Zig FFI correctly implements the Idris2 ABI. +// They exercise the exported C-ABI functions end-to-end. + +const std = @import("std"); +const testing = std.testing; + +// Import FFI functions +extern fn gqldt_init() callconv(.c) i32; +extern fn gqldt_cleanup() callconv(.c) void; +extern fn gqldt_db_open(path: [*:0]const u8, path_len: u64, db_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_db_close(db: *anyopaque) callconv(.c) i32; +extern fn gqldt_parse(query_str: [*:0]const u8, query_len: u64, query_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_parse_inferred(query_str: [*:0]const u8, query_len: u64, schema: *anyopaque, query_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_typecheck(query: *anyopaque, schema: *anyopaque) callconv(.c) i32; +extern fn gqldt_execute(db: *anyopaque, query: *anyopaque, result_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_get_schema(db: *anyopaque, collection_name: [*:0]const u8, schema_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_query_free(query: *anyopaque) callconv(.c) void; +extern fn gqldt_schema_free(schema: *anyopaque) callconv(.c) void; +extern fn gqldt_result_free(result: *anyopaque) callconv(.c) void; +extern fn gqldt_serialize_cbor(query: *anyopaque, buffer: [*]u8, buffer_len: u64, written_out: *u64) callconv(.c) i32; +extern fn gqldt_serialize_json(query: *anyopaque, buffer: [*]u8, buffer_len: u64, written_out: *u64) callconv(.c) i32; +extern fn gqldt_deserialize_cbor(buffer: [*]const u8, buffer_len: u64, query_out: *?*anyopaque) callconv(.c) i32; +extern fn gqldt_validate_permissions(query: *anyopaque, user_id: [*:0]const u8, permissions: *const anyopaque) callconv(.c) i32; +extern fn gqldt_version() callconv(.c) [*:0]const u8; +extern fn gqldt_alloc_slot() callconv(.c) ?*anyopaque; +extern fn gqldt_read_slot(slot: *anyopaque) callconv(.c) u64; +extern fn gqldt_free_slot(slot: *anyopaque) callconv(.c) void; +extern fn gqldt_bits64_to_ptr(value: u64) callconv(.c) ?*anyopaque; + +//============================================================================== +// Lifecycle Tests +//============================================================================== + +test "init and cleanup" { + const status = gqldt_init(); + try testing.expectEqual(@as(i32, 0), status); + gqldt_cleanup(); +} + +test "double init is idempotent" { + _ = gqldt_init(); + const status = gqldt_init(); + try testing.expectEqual(@as(i32, 0), status); + gqldt_cleanup(); +} + +//============================================================================== +// Database Operations +//============================================================================== + +test "db_open and db_close" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*anyopaque = null; + const open_status = gqldt_db_open("test.db", 7, &db); + try testing.expectEqual(@as(i32, 0), open_status); + try testing.expect(db != null); + + const close_status = gqldt_db_close(db.?); + try testing.expectEqual(@as(i32, 0), close_status); +} + +test "db_open rejects empty path" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*anyopaque = null; + const status = gqldt_db_open("", 0, &db); + try testing.expectEqual(@as(i32, 1), status); // invalid_arg +} + +test "db_open rejects oversized path" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*anyopaque = null; + const status = gqldt_db_open("x", 5000, &db); + try testing.expectEqual(@as(i32, 1), status); // invalid_arg +} + +//============================================================================== +// Query Parsing +//============================================================================== + +test "parse valid query" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*anyopaque = null; + const status = gqldt_parse("MATCH (n:Person) RETURN n", 25, &query); + try testing.expectEqual(@as(i32, 0), status); + try testing.expect(query != null); + + gqldt_query_free(query.?); +} + +test "parse rejects empty query" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*anyopaque = null; + const status = gqldt_parse("", 0, &query); + try testing.expectEqual(@as(i32, 1), status); // invalid_arg +} + +//============================================================================== +// Type Checking and Execution +//============================================================================== + +test "full pipeline: open -> schema -> parse -> typecheck -> execute" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + // Open database. + var db: ?*anyopaque = null; + _ = gqldt_db_open("pipeline.db", 11, &db); + defer gqldt_db_close(db.?); + + // Get schema. + var schema: ?*anyopaque = null; + _ = gqldt_get_schema(db.?, "nodes", &schema); + defer gqldt_schema_free(schema.?); + + // Parse query. + var query: ?*anyopaque = null; + _ = gqldt_parse("MATCH (n) RETURN n", 18, &query); + defer gqldt_query_free(query.?); + + // Type-check. + const tc_status = gqldt_typecheck(query.?, schema.?); + try testing.expectEqual(@as(i32, 0), tc_status); + + // Execute. + var result: ?*anyopaque = null; + const exec_status = gqldt_execute(db.?, query.?, &result); + try testing.expectEqual(@as(i32, 0), exec_status); + try testing.expect(result != null); + + gqldt_result_free(result.?); +} + +test "execute without typecheck fails" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var db: ?*anyopaque = null; + _ = gqldt_db_open("test.db", 7, &db); + defer gqldt_db_close(db.?); + + var query: ?*anyopaque = null; + _ = gqldt_parse("MATCH (n) RETURN n", 18, &query); + defer gqldt_query_free(query.?); + + var result: ?*anyopaque = null; + const status = gqldt_execute(db.?, query.?, &result); + try testing.expectEqual(@as(i32, 2), status); // type_mismatch +} + +//============================================================================== +// Serialization +//============================================================================== + +test "serialize to CBOR and back" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*anyopaque = null; + _ = gqldt_parse("RETURN 42", 9, &query); + defer gqldt_query_free(query.?); + + var cbor_buf: [1024]u8 = undefined; + var written: u64 = 0; + const ser_status = gqldt_serialize_cbor(query.?, &cbor_buf, 1024, &written); + try testing.expectEqual(@as(i32, 0), ser_status); + try testing.expect(written > 0); + + var query2: ?*anyopaque = null; + const deser_status = gqldt_deserialize_cbor(&cbor_buf, written, &query2); + try testing.expectEqual(@as(i32, 0), deser_status); + try testing.expect(query2 != null); + + gqldt_query_free(query2.?); +} + +test "serialize to JSON" { + _ = gqldt_init(); + defer gqldt_cleanup(); + + var query: ?*anyopaque = null; + _ = gqldt_parse("RETURN 1", 8, &query); + defer gqldt_query_free(query.?); + + var json_buf: [1024]u8 = undefined; + var written: u64 = 0; + const status = gqldt_serialize_json(query.?, &json_buf, 1024, &written); + try testing.expectEqual(@as(i32, 0), status); + + const json_str = json_buf[0..@intCast(written)]; + try testing.expectEqualStrings("{\"query\":\"RETURN 1\"}", json_str); +} + +//============================================================================== +// Slot Allocation Helpers +//============================================================================== + +test "alloc_slot and free_slot" { + const slot = gqldt_alloc_slot() orelse return error.SlotAllocFailed; + const value = gqldt_read_slot(slot); + try testing.expectEqual(@as(u64, 0), value); // initially zero + gqldt_free_slot(slot); +} + +test "bits64_to_ptr null returns null" { + const ptr = gqldt_bits64_to_ptr(0); + try testing.expect(ptr == null); +} + +//============================================================================== +// Version +//============================================================================== + +test "version string is semantic version" { + const ver = gqldt_version(); + const ver_str = std.mem.span(ver); + try testing.expect(ver_str.len > 0); + try testing.expect(std.mem.count(u8, ver_str, ".") >= 1); +}