Skip to content

Commit e54cd96

Browse files
refactor: semantic port to AffineScript (#126)
Automated semantic porting.
1 parent fcad257 commit e54cd96

3 files changed

Lines changed: 598 additions & 9 deletions

File tree

‎bindings/rescript/Bunsenite.affine‎

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,176 @@
11
// SPDX-License-Identifier: MPL-2.0
2-
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3-
// Ported via Harvard Engine bulk-processor
2+
// Ported via Harvard Engine (Semantic pass)
43

54
module Bunsenite;
65

7-
// TODO: Complete semantic implementation
6+
// Bunsenite Rescript Bindings
7+
// Type-safe Rescript bindings for Bunsenite via C FFI
8+
//
9+
// Usage:
10+
// open Bunsenite
11+
// fn config = parseNickel("{foo = 42}", "config.ncl")
12+
// Js.log(config)
13+
14+
// External C FFI declarations
15+
// These bind to the C ABI provided by the Zig layer
16+
17+
@module("./bunsenite_ffi")
18+
external parseNickelRaw: (string, string) => Js.Nullable.t<string> = "parse_nickel"
19+
20+
@module("./bunsenite_ffi")
21+
external validateNickelRaw: (string, string) => int = "validate_nickel"
22+
23+
@module("./bunsenite_ffi")
24+
external versionRaw: unit => string = "version"
25+
26+
@module("./bunsenite_ffi")
27+
external rsrTierRaw: unit => string = "rsr_tier"
28+
29+
@module("./bunsenite_ffi")
30+
external tpcfPerimeterRaw: unit => int = "tpcf_perimeter"
31+
32+
// Result struct for error handling
33+
struct result<'a, 'e> = Ok('a) | Error('e)
34+
35+
// Error struct
36+
struct error {
37+
| ParseError(string)
38+
| ValidationError(string)
39+
| InvalidInput(string)
40+
41+
// Parse and evaluate a Nickel configuration string
42+
//
43+
// Example:
44+
// fn config = parseNickel("{name = \"example\", port = 8080}", "config.ncl")
45+
// switch config {
46+
// | Ok(json) => Js.log(json)
47+
// | Error(err) => Js.log2("Error:", err)
48+
// }
49+
fn parseNickel = (source: string, name: string): result<Js.Json.t, error> => {
50+
fn result = parseNickelRaw(source, name)
51+
52+
switch Js.Nullable.toOption(result) {
53+
| Some(jsonString) =>
54+
try {
55+
fn parsed = Js.Json.parseExn(jsonString)
56+
Ok(parsed)
57+
} catch {
58+
| _ => Error(ParseError("Failed to parse JSON result"))
59+
}
60+
| None => Error(ParseError("Failed to parse Nickel configuration: " ++ name))
61+
}
62+
}
63+
64+
// Validate a Nickel configuration without evaluating it
65+
//
66+
// Example:
67+
// fn result = validateNickel("{foo = 42}", "config.ncl")
68+
// switch result {
69+
// | Ok() => Js.log("Valid!")
70+
// | Error(err) => Js.log2("Invalid:", err)
71+
// }
72+
fn validateNickel = (source: string, name: string): result<unit, error> => {
73+
fn resultCode = validateNickelRaw(source, name)
74+
75+
if resultCode == 0 {
76+
Ok()
77+
} else {
78+
Error(ValidationError("Validation failed for: " ++ name))
79+
}
80+
}
81+
82+
// Get library version
83+
//
84+
// Example:
85+
// fn ver = getVersion()
86+
// Js.log2("Version:", ver)
87+
fn getVersion = (): string => {
88+
versionRaw()
89+
}
90+
91+
// Get RSR compliance tier
92+
//
93+
// Example:
94+
// fn tier = getRSRTier()
95+
// Js.log2("RSR Tier:", tier)
96+
fn getRSRTier = (): string => {
97+
rsrTierRaw()
98+
}
99+
100+
// Get TPCF perimeter number
101+
//
102+
// Example:
103+
// fn perimeter = getTPCFPerimeter()
104+
// Js.log2("TPCF Perimeter:", perimeter)
105+
fn getTPCFPerimeter = (): int => {
106+
tpcfPerimeterRaw()
107+
}
108+
109+
// Helper: Parse Nickel file from filesystem
110+
// Requires Node.js fs module
111+
//
112+
// Example:
113+
// fn config = parseFile("./config.ncl")
114+
// switch config {
115+
// | Ok(json) => Js.log(json)
116+
// | Error(err) => Js.log2("Error:", err)
117+
// }
118+
@module("fs")
119+
external readFileSync: (string, string) => string = "readFileSync"
120+
121+
fn parseFile = (path: string): result<Js.Json.t, error> => {
122+
try {
123+
fn source = readFileSync(path, "utf8")
124+
parseNickel(source, path)
125+
} catch {
126+
| _ => Error(InvalidInput("Failed to read file: " ++ path))
127+
}
128+
}
129+
130+
// Helper: Validate Nickel file from filesystem
131+
//
132+
// Example:
133+
// fn result = validateFile("./config.ncl")
134+
// switch result {
135+
// | Ok() => Js.log("Valid!")
136+
// | Error(err) => Js.log2("Invalid:", err)
137+
// }
138+
fn validateFile = (path: string): result<unit, error> => {
139+
try {
140+
fn source = readFileSync(path, "utf8")
141+
validateNickel(source, path)
142+
} catch {
143+
| _ => Error(InvalidInput("Failed to read file: " ++ path))
144+
}
145+
}
146+
147+
// Helper: Get config value by key path
148+
// Example: getConfigValue(config, ["server", "port"])
149+
fn rec getConfigValue = (json: Js.Json.t, path: list<string>): option<Js.Json.t> => {
150+
switch path {
151+
| list{} => Some(json)
152+
| list{key, ...rest} =>
153+
switch Js.Json.decodeObject(json) {
154+
| Some(obj) =>
155+
switch Js.Dict.get(obj, key) {
156+
| Some(value) => getConfigValue(value, rest)
157+
| None => None
158+
}
159+
| None => None
160+
}
161+
}
162+
}
163+
164+
// Helper: Convert error to string for display
165+
fn errorToString = (err: error): string => {
166+
switch err {
167+
| ParseError(msg) => "Parse Error: " ++ msg
168+
| ValidationError(msg) => "Validation Error: " ++ msg
169+
| InvalidInput(msg) => "Invalid Input: " ++ msg
170+
}
171+
}
172+
173+
// Re-export result struct for convenience
174+
struct parseResult { result<Js.Json.t, error>
175+
struct validateResult { result<unit, error>
176+
Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,184 @@
11
// SPDX-License-Identifier: MPL-2.0
2-
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3-
// Ported via Harvard Engine bulk-processor
2+
// Ported via Harvard Engine (Semantic pass)
43

54
module Bunsenite_test;
65

7-
// TODO: Complete semantic implementation
6+
// SPDX-License-Identifier: MPL-2.0
7+
// Bunsenite ReScript Bindings Test Suite
8+
9+
open Bunsenite
10+
11+
// Test helpers
12+
fn assertEqual = (actual, expected, testName) => {
13+
if actual == expected {
14+
Console.log(`✓ ${testName}`)
15+
} else {
16+
Console.error(`✗ ${testName}`)
17+
Console.error(` Expected: ${expected->Js.Json.stringify}`)
18+
Console.error(` Actual: ${actual->Js.Json.stringify}`)
19+
}
20+
}
21+
22+
fn assertOk = (result, testName) => {
23+
switch result {
24+
| Ok(_) => Console.log(`✓ ${testName}`)
25+
| Error(err) => {
26+
Console.error(`✗ ${testName}`)
27+
Console.error(` Error: ${errorToString(err)}`)
28+
}
29+
}
30+
}
31+
32+
fn assertError = (result, testName) => {
33+
switch result {
34+
| Error(_) => Console.log(`✓ ${testName}`)
35+
| Ok(_) => Console.error(`✗ ${testName}: Expected error but got Ok`)
36+
}
37+
}
38+
39+
// Test suite
40+
fn runTests = () => {
41+
Console.log("\n🧪 Bunsenite ReScript Bindings Test Suite\n")
42+
43+
// Test 1: Parse simple Nickel configuration
44+
Console.log("Parse Tests:")
45+
fn simpleConfig = parseNickel("{foo = 42}", "test.ncl")
46+
assertOk(simpleConfig, "Parse simple number configuration")
47+
48+
// Test 2: Parse object configuration
49+
fn objectConfig = parseNickel("{name = \"test\", value = 100}", "object.ncl")
50+
assertOk(objectConfig, "Parse object configuration")
51+
52+
// Test 3: Parse nested configuration
53+
fn nestedConfig = parseNickel("{server = {port = 8080, host = \"localhost\"}}", "nested.ncl")
54+
assertOk(nestedConfig, "Parse nested configuration")
55+
56+
// Test 4: Parse array configuration
57+
fn arrayConfig = parseNickel("{items = [1, 2, 3, 4, 5]}", "array.ncl")
58+
assertOk(arrayConfig, "Parse array configuration")
59+
60+
// Test 5: Parse invalid syntax (should error)
61+
fn invalidConfig = parseNickel("{foo = }", "invalid.ncl")
62+
assertError(invalidConfig, "Parse invalid syntax returns error")
63+
64+
// Test 6: Parse empty configuration
65+
fn emptyConfig = parseNickel("{}", "empty.ncl")
66+
assertOk(emptyConfig, "Parse empty configuration")
67+
68+
// Validation Tests
69+
Console.log("\nValidation Tests:")
70+
71+
fn validConfig = validateNickel("{foo = 42}", "valid.ncl")
72+
assertOk(validConfig, "Validate correct configuration")
73+
74+
fn invalidValidation = validateNickel("{foo = }", "invalid-validate.ncl")
75+
assertError(invalidValidation, "Validate incorrect configuration returns error")
76+
77+
// Test 7: Validate complex configuration
78+
fn complexValid = validateNickel(
79+
"{
80+
app = {
81+
name = \"example\",
82+
version = \"1.0.0\",
83+
config = {
84+
debug = true,
85+
port = 3000
86+
}
87+
}
88+
}",
89+
"complex.ncl",
90+
)
91+
assertOk(complexValid, "Validate complex nested configuration")
92+
93+
// Library Info Tests
94+
Console.log("\nLibrary Info Tests:")
95+
96+
fn version = getVersion()
97+
Console.log(`✓ Got version: ${version}`)
98+
99+
fn tier = getRSRTier()
100+
Console.log(`✓ Got RSR tier: ${tier}`)
101+
102+
fn perimeter = getTPCFPerimeter()
103+
Console.log(`✓ Got TPCF perimeter: ${perimeter->Int.toString}`)
104+
105+
// Config Value Tests
106+
Console.log("\nConfig Value Extraction Tests:")
107+
108+
switch objectConfig {
109+
| Ok(json) => {
110+
// Test extracting top-level value
111+
fn nameValue = getConfigValue(json, list{"name"})
112+
switch nameValue {
113+
| Some(_) => Console.log("✓ Extract top-level value")
114+
| None => Console.error("✗ Failed to extract top-level value")
115+
}
116+
117+
// Test extracting non-existent value
118+
fn missingValue = getConfigValue(json, list{"missing"})
119+
switch missingValue {
120+
| None => Console.log("✓ Non-existent value returns None")
121+
| Some(_) => Console.error("✗ Non-existent value should return None")
122+
}
123+
}
124+
| Error(_) => Console.error("✗ Could not test config value extraction")
125+
}
126+
127+
switch nestedConfig {
128+
| Ok(json) => {
129+
// Test extracting nested value
130+
fn portValue = getConfigValue(json, list{"server", "port"})
131+
switch portValue {
132+
| Some(_) => Console.log("✓ Extract nested value")
133+
| None => Console.error("✗ Failed to extract nested value")
134+
}
135+
136+
// Test extracting with invalid path
137+
fn invalidPath = getConfigValue(json, list{"server", "nonexistent", "deep"})
138+
switch invalidPath {
139+
| None => Console.log("✓ Invalid nested path returns None")
140+
| Some(_) => Console.error("✗ Invalid path should return None")
141+
}
142+
}
143+
| Error(_) => Console.error("✗ Could not test nested value extraction")
144+
}
145+
146+
// Error handling tests
147+
Console.log("\nError Handling Tests:")
148+
149+
fn parseErr = parseNickel("{invalid syntax here}", "error-test.ncl")
150+
switch parseErr {
151+
| Error(err) => {
152+
fn errStr = errorToString(err)
153+
Console.log(`✓ Error converted to string: ${errStr}`)
154+
}
155+
| Ok(_) => Console.error("✗ Expected parse error")
156+
}
157+
158+
// Result struct tests
159+
Console.log("\nResult Type Tests:")
160+
161+
fn successResult: parseResult = Ok(Js.Json.null)
162+
switch successResult {
163+
| Ok(_) => Console.log("✓ parseResult Ok variant works")
164+
| Error(_) => Console.error("✗ parseResult Ok variant failed")
165+
}
166+
167+
fn errorResult: parseResult = Error(ParseError("test"))
168+
switch errorResult {
169+
| Error(_) => Console.log("✓ parseResult Error variant works")
170+
| Ok(_) => Console.error("✗ parseResult Error variant failed")
171+
}
172+
173+
fn validateSuccess: validateResult = Ok()
174+
switch validateSuccess {
175+
| Ok() => Console.log("✓ validateResult Ok variant works")
176+
| Error(_) => Console.error("✗ validateResult Ok variant failed")
177+
}
178+
179+
Console.log("\n✅ Test suite complete\n")
180+
}
181+
182+
// Run tests
183+
runTests()
184+

0 commit comments

Comments
 (0)