Skip to content

Commit 4a1a272

Browse files
hyperpolymathclaude
andcommitted
feat(lol/zig-gateway): add lol_ffi + types modules; fix proven lib path
Adds the two Zig modules the gateway needs to call through to liblol (the LOL/1000Langs i18n C ABI) and the proven-library FFI in a single well-typed surface: - `src/lol_ffi.zig` — Zig wrapper over liblol, exposing a safe, allocator-aware API for the gateway's downstream callers. - `src/types.zig` — LOL public-domain types reused by the gateway. Also corrects `build.zig` to point at proven's standard `ffi/zig/zig-out/lib` instead of the retired `zig-out-standalone/lib` symlink (removed 2026-04-17). Comment updated to explain the move for future readers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 095cf11 commit 4a1a272

3 files changed

Lines changed: 506 additions & 2 deletions

File tree

lol/api/zig-gateway/build.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ const DEFAULT_ZIG_API_LIB_PATH =
4747
const DEFAULT_ZIG_API_INCLUDE_PATH =
4848
"/var/mnt/eclipse/repos/developer-ecosystem/zig-api/generated/abi";
4949

50-
/// Pre-built libproven_ffi (verification-ecosystem/proven/ffi/zig/zig-out-standalone/lib).
50+
/// Pre-built libproven_ffi (verification-ecosystem/proven/ffi/zig/zig-out/lib).
51+
/// Points to proven's standard zig-out/lib output (zig-out-standalone symlink removed 2026-04-17).
5152
const DEFAULT_PROVEN_LIB_PATH =
52-
"/var/mnt/eclipse/repos/verification-ecosystem/proven/ffi/zig/zig-out-standalone/lib";
53+
"/var/mnt/eclipse/repos/verification-ecosystem/proven/ffi/zig/zig-out/lib";
5354

5455
pub fn build(b: *std.Build) void {
5556
const target = b.standardTargetOptions(.{});
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// lol_ffi.zig — Zig wrapper around liblol (the LOL i18n C ABI).
5+
//
6+
// Replaces:
7+
// api/v-lol/src/lol.v (Service struct + public methods)
8+
// api/v-lol/src/ffi.v (raw C declarations — inlined here)
9+
//
10+
// This module calls the C functions declared in generated/abi/lol.h and
11+
// converts their output into idiomatic Zig types defined in types.zig.
12+
// All memory from liblol is freed here before returning; callers receive
13+
// only owned Zig values (backed by their own allocators).
14+
//
15+
// The Service struct is the primary API surface — create one with
16+
// Service.open(), use it, then call deinit() to release the handle.
17+
18+
const std = @import("std");
19+
const types = @import("types.zig");
20+
21+
// =============================================================================
22+
// C ABI declarations — mirrors ffi.v (inlined, no separate file needed)
23+
// =============================================================================
24+
// These extern declarations match generated/abi/lol.h exactly.
25+
// The build system passes -llol so the linker resolves them to liblol.
26+
27+
// Opaque C types for the structs. We read fields via @field after a
28+
// @ptrCast to the typed C struct pointer — but we never expose the C
29+
// pointers outside this module.
30+
31+
const CLolLocale = extern struct {
32+
tag: ?[*:0]const u8,
33+
language: ?[*:0]const u8,
34+
script: ?[*:0]const u8,
35+
region: ?[*:0]const u8,
36+
};
37+
38+
const CLolTranslationResult = extern struct {
39+
text: ?[*:0]const u8,
40+
resolved_locale: ?[*:0]const u8,
41+
is_fallback: u32,
42+
_padding: u32,
43+
};
44+
45+
const CLolLanguageInfo = extern struct {
46+
iso639_3: ?[*:0]const u8,
47+
name: ?[*:0]const u8,
48+
native_name: ?[*:0]const u8,
49+
family: ?[*:0]const u8,
50+
scripts: ?[*:0]const u8,
51+
source_count: u32,
52+
verse_count: u32,
53+
quality: f64,
54+
};
55+
56+
const CLolPluralRule = extern struct {
57+
language: ?[*:0]const u8,
58+
form_count: u32,
59+
categories: [6]u32,
60+
_padding: u32,
61+
};
62+
63+
// Library lifecycle
64+
extern fn lol_init(data_dir: [*:0]const u8) ?*anyopaque;
65+
extern fn lol_free(handle: *anyopaque) void;
66+
extern fn lol_is_initialized(handle: *anyopaque) u32;
67+
68+
// Locale resolution
69+
extern fn lol_resolve_locale(handle: *anyopaque, tag: [*:0]const u8) ?*CLolLocale;
70+
extern fn lol_free_locale(locale: *CLolLocale) void;
71+
72+
// Translation
73+
extern fn lol_translate(handle: *anyopaque, locale_tag: [*:0]const u8, key: [*:0]const u8) ?*CLolTranslationResult;
74+
extern fn lol_free_translation(result: *CLolTranslationResult) void;
75+
extern fn lol_translation_text(result: *CLolTranslationResult) ?[*:0]const u8;
76+
77+
// Plural
78+
extern fn lol_select_plural(handle: *anyopaque, locale_tag: [*:0]const u8, quantity: u64) u32;
79+
extern fn lol_translate_plural(handle: *anyopaque, locale_tag: [*:0]const u8, key: [*:0]const u8, quantity: u64) ?*CLolTranslationResult;
80+
81+
// Language metadata
82+
extern fn lol_language_count(handle: *anyopaque) u32;
83+
extern fn lol_get_language(handle: *anyopaque, code: [*:0]const u8) ?*CLolLanguageInfo;
84+
extern fn lol_free_language(info: *CLolLanguageInfo) void;
85+
86+
// Plural rules
87+
extern fn lol_get_plural_rule(handle: *anyopaque, lang_code: [*:0]const u8) ?*CLolPluralRule;
88+
extern fn lol_free_plural_rule(rule: *CLolPluralRule) void;
89+
90+
// Fallback chain
91+
extern fn lol_fallback_chain_len(handle: *anyopaque, locale_tag: [*:0]const u8) u32;
92+
93+
// Error / version
94+
extern fn lol_last_error() ?[*:0]const u8;
95+
extern fn lol_version() ?[*:0]const u8;
96+
extern fn lol_build_info() ?[*:0]const u8;
97+
98+
// =============================================================================
99+
// Internal helpers
100+
// =============================================================================
101+
102+
/// Convert an optional C null-terminated string pointer to a Zig slice.
103+
/// Returns an empty slice when the pointer is null.
104+
/// The returned slice points into memory owned by liblol; callers must copy
105+
/// before the owning C struct is freed.
106+
fn cStrToSlice(ptr: ?[*:0]const u8) []const u8 {
107+
const p = ptr orelse return "";
108+
return std.mem.span(p);
109+
}
110+
111+
/// Duplicate a slice into allocator-owned memory. Used to copy strings out
112+
/// of C structs before we call the corresponding lol_free_* function.
113+
fn dupeStr(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
114+
return allocator.dupe(u8, s);
115+
}
116+
117+
// =============================================================================
118+
// Service — public API, mirrors lol.v
119+
// =============================================================================
120+
121+
/// Primary interface to the LOL i18n service. Wraps an opaque liblol handle.
122+
///
123+
/// Usage:
124+
/// var svc = try Service.open(allocator, "corpus");
125+
/// defer svc.deinit();
126+
/// const result = try svc.translate(allocator, "en-US", "greeting");
127+
/// // use result.text ...
128+
pub const Service = struct {
129+
handle: *anyopaque,
130+
131+
// -------------------------------------------------------------------------
132+
// Lifecycle
133+
// -------------------------------------------------------------------------
134+
135+
/// Open a LOL service instance, loading corpus data from `data_dir`.
136+
/// Returns `error.GenericError` (with last_error() populated) on failure.
137+
pub fn open(data_dir: [:0]const u8) types.LolError!Service {
138+
const h = lol_init(data_dir.ptr) orelse return error.GenericError;
139+
return Service{ .handle = h };
140+
}
141+
142+
/// Open with the default corpus directory ("corpus").
143+
pub fn openDefault() types.LolError!Service {
144+
return open("corpus");
145+
}
146+
147+
/// Release all resources. Safe to call exactly once; the Service must
148+
/// not be used after deinit().
149+
pub fn deinit(self: *Service) void {
150+
lol_free(self.handle);
151+
// Poison the pointer so use-after-free is detectable in debug builds.
152+
self.handle = @ptrFromInt(0xDEAD_BEEF_DEAD_BEEF);
153+
}
154+
155+
/// Return true if the handle is valid and ready for calls.
156+
pub fn isInitialized(self: *const Service) bool {
157+
return lol_is_initialized(self.handle) == 1;
158+
}
159+
160+
// -------------------------------------------------------------------------
161+
// Locale resolution
162+
// -------------------------------------------------------------------------
163+
164+
/// Parse and resolve a BCP 47 locale tag against the corpus.
165+
/// The returned Locale's string fields are allocated from `allocator`.
166+
pub fn resolveLocale(
167+
self: *const Service,
168+
allocator: std.mem.Allocator,
169+
tag: [:0]const u8,
170+
) (types.LolError || std.mem.Allocator.Error)!types.Locale {
171+
const raw = lol_resolve_locale(self.handle, tag.ptr) orelse
172+
return error.LocaleNotFound;
173+
defer lol_free_locale(raw);
174+
175+
return types.Locale{
176+
.tag = try dupeStr(allocator, cStrToSlice(raw.tag)),
177+
.language = try dupeStr(allocator, cStrToSlice(raw.language)),
178+
.script = try dupeStr(allocator, cStrToSlice(raw.script)),
179+
.region = try dupeStr(allocator, cStrToSlice(raw.region)),
180+
};
181+
}
182+
183+
// -------------------------------------------------------------------------
184+
// Translation
185+
// -------------------------------------------------------------------------
186+
187+
/// Look up a translation key for the given locale. Walks the fallback
188+
/// chain (e.g. en-US → en → default) when the exact locale is missing.
189+
/// The returned TranslationResult's string fields are allocated from
190+
/// `allocator`.
191+
pub fn translate(
192+
self: *const Service,
193+
allocator: std.mem.Allocator,
194+
locale_tag: [:0]const u8,
195+
key: [:0]const u8,
196+
) (types.LolError || std.mem.Allocator.Error)!types.TranslationResult {
197+
const raw = lol_translate(self.handle, locale_tag.ptr, key.ptr) orelse
198+
return error.KeyNotFound;
199+
defer lol_free_translation(raw);
200+
201+
return types.TranslationResult{
202+
.text = try dupeStr(allocator, cStrToSlice(raw.text)),
203+
.resolved_locale = try dupeStr(allocator, cStrToSlice(raw.resolved_locale)),
204+
.is_fallback = raw.is_fallback != 0,
205+
};
206+
}
207+
208+
/// Combine plural form selection with a translation lookup.
209+
/// The `quantity` determines the CLDR plural category, which is appended
210+
/// as a suffix to the key (e.g. "items" → "items.one").
211+
pub fn translatePlural(
212+
self: *const Service,
213+
allocator: std.mem.Allocator,
214+
locale_tag: [:0]const u8,
215+
key: [:0]const u8,
216+
quantity: u64,
217+
) (types.LolError || std.mem.Allocator.Error)!types.TranslationResult {
218+
const raw = lol_translate_plural(self.handle, locale_tag.ptr, key.ptr, quantity) orelse
219+
return error.KeyNotFound;
220+
defer lol_free_translation(raw);
221+
222+
return types.TranslationResult{
223+
.text = try dupeStr(allocator, cStrToSlice(raw.text)),
224+
.resolved_locale = try dupeStr(allocator, cStrToSlice(raw.resolved_locale)),
225+
.is_fallback = raw.is_fallback != 0,
226+
};
227+
}
228+
229+
// -------------------------------------------------------------------------
230+
// Plural selection
231+
// -------------------------------------------------------------------------
232+
233+
/// Return the CLDR plural category for `quantity` in the given locale.
234+
/// Never fails — values outside the 0–5 ABI range map to `.other`.
235+
pub fn selectPlural(
236+
self: *const Service,
237+
locale_tag: [:0]const u8,
238+
quantity: u64,
239+
) types.PluralCategory {
240+
const raw = lol_select_plural(self.handle, locale_tag.ptr, quantity);
241+
return types.PluralCategory.fromInt(raw);
242+
}
243+
244+
// -------------------------------------------------------------------------
245+
// Language metadata
246+
// -------------------------------------------------------------------------
247+
248+
/// Return the number of languages available in the corpus.
249+
pub fn languageCount(self: *const Service) u32 {
250+
return lol_language_count(self.handle);
251+
}
252+
253+
/// Look up metadata for an ISO 639-3 language code (e.g. "eng").
254+
/// String fields in the returned LanguageInfo are allocated from
255+
/// `allocator`.
256+
pub fn getLanguage(
257+
self: *const Service,
258+
allocator: std.mem.Allocator,
259+
code: [:0]const u8,
260+
) (types.LolError || std.mem.Allocator.Error)!types.LanguageInfo {
261+
const raw = lol_get_language(self.handle, code.ptr) orelse
262+
return error.LocaleNotFound;
263+
defer lol_free_language(raw);
264+
265+
return types.LanguageInfo{
266+
.iso639_3 = try dupeStr(allocator, cStrToSlice(raw.iso639_3)),
267+
.name = try dupeStr(allocator, cStrToSlice(raw.name)),
268+
.native_name = try dupeStr(allocator, cStrToSlice(raw.native_name)),
269+
.family = try dupeStr(allocator, cStrToSlice(raw.family)),
270+
.scripts = try dupeStr(allocator, cStrToSlice(raw.scripts)),
271+
.source_count = raw.source_count,
272+
.verse_count = raw.verse_count,
273+
.quality = raw.quality,
274+
};
275+
}
276+
277+
// -------------------------------------------------------------------------
278+
// Plural rules
279+
// -------------------------------------------------------------------------
280+
281+
/// Retrieve the CLDR plural rule for a language.
282+
/// `categories` slice in the returned PluralRule is allocated from
283+
/// `allocator`; string fields are also allocator-owned.
284+
pub fn getPluralRule(
285+
self: *const Service,
286+
allocator: std.mem.Allocator,
287+
lang_code: [:0]const u8,
288+
) (types.LolError || std.mem.Allocator.Error)!types.PluralRule {
289+
const raw = lol_get_plural_rule(self.handle, lang_code.ptr) orelse
290+
return error.LocaleNotFound;
291+
defer lol_free_plural_rule(raw);
292+
293+
// form_count is proven to be 1–6 in the Idris2 ABI; cap defensively.
294+
const count: u32 = @min(raw.form_count, 6);
295+
const cats = try allocator.alloc(types.PluralCategory, count);
296+
for (cats, 0..) |*slot, i| {
297+
slot.* = types.PluralCategory.fromInt(raw.categories[i]);
298+
}
299+
300+
return types.PluralRule{
301+
.language = try dupeStr(allocator, cStrToSlice(raw.language)),
302+
.form_count = count,
303+
.categories = cats,
304+
};
305+
}
306+
307+
// -------------------------------------------------------------------------
308+
// Fallback chain
309+
// -------------------------------------------------------------------------
310+
311+
/// Return the number of locales in the fallback chain for `locale_tag`.
312+
/// For "en-GB" this is typically 3: en-GB → en → default.
313+
pub fn fallbackChainLength(
314+
self: *const Service,
315+
locale_tag: [:0]const u8,
316+
) u32 {
317+
return lol_fallback_chain_len(self.handle, locale_tag.ptr);
318+
}
319+
};
320+
321+
// =============================================================================
322+
// Module-level helpers (no Service required) — mirrors module-level fns in lol.v
323+
// =============================================================================
324+
325+
/// Return the most recent liblol error message, or an empty slice.
326+
pub fn lastError() []const u8 {
327+
return cStrToSlice(lol_last_error());
328+
}
329+
330+
/// Return the liblol version string.
331+
pub fn version() []const u8 {
332+
return cStrToSlice(lol_version());
333+
}
334+
335+
/// Return the liblol build information string.
336+
pub fn buildInfo() []const u8 {
337+
return cStrToSlice(lol_build_info());
338+
}

0 commit comments

Comments
 (0)