From 8c1343aaf2778336b7f413b1ba7a99cf406218bf Mon Sep 17 00:00:00 2001 From: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:43:34 -0300 Subject: [PATCH] refactor(codegen): shared FFIType IR + single type parser for C/C++/Rust (#104) --- ffi/codegen/c.nim | 162 +++++++++++++------------------ ffi/codegen/c_cpp_common.nim | 10 -- ffi/codegen/cpp.nim | 60 ++++++------ ffi/codegen/rust.nim | 57 ++++++----- ffi/codegen/string_helpers.nim | 2 +- ffi/codegen/types_ir.nim | 134 +++++++++++++++++++++++++ tests/unit/test_rust_codegen.nim | 43 ++++++++ tests/unit/test_types_ir.nim | 122 +++++++++++++++++++++++ 8 files changed, 429 insertions(+), 161 deletions(-) create mode 100644 ffi/codegen/types_ir.nim create mode 100644 tests/unit/test_rust_codegen.nim create mode 100644 tests/unit/test_types_ir.nim diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index 57ef9ca..f841995 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -13,7 +13,7 @@ ## distinctly-named codec emitted by the cbor_helpers template. import std/[os, strutils, tables, sets] -import ./meta, ./string_helpers, ./c_cpp_common +import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir ## Wire-format C type for any Nim `ptr T` / `pointer`. Fixed 64-bit so the CBOR ## payload size is stable regardless of host architecture (mirrors CppPtrType). @@ -30,81 +30,43 @@ const PreludeHeaderName* = "nim_ffi_prelude.h" CborHeaderName* = "nim_ffi_cbor.h" -type LeafInfo = tuple[ok: bool, cType: string, suffix: string, owns: bool] - -func leafCType(t: string): LeafInfo = - ## Maps a Nim leaf type to its C type, codec suffix and whether a decoded - ## value owns heap memory. `ok` is false for composite types (seq/Option/ - ## user structs), which are monomorphised separately. - case t - of "int", "int64": - (true, "int64_t", "i64", false) - of "int32": - (true, "int32_t", "i32", false) - of "int16": - (true, "int16_t", "i16", false) - of "int8": - (true, "int8_t", "i8", false) - of "uint", "uint64": - (true, "uint64_t", "u64", false) - of "uint32": - (true, "uint32_t", "u32", false) - of "uint16": - (true, "uint16_t", "u16", false) - of "uint8", "byte": - (true, "uint8_t", "u8", false) - of "bool": - (true, "bool", "bool", false) - of "float", "float64": - (true, "double", "f64", false) - of "float32": - (true, "float", "f32", false) - of "pointer": - (true, CPtrType, "u64", false) - of "string", "cstring": - (true, "NimFfiStr", "str", true) - else: - (false, "", "", false) - -func cToken(cType: string): string = - ## Short PascalCase token used to build monomorphised container names and - ## codec-adapter symbols. Composite C type names are already unique C - ## identifiers, so they pass through verbatim. - case cType - of "int64_t": "I64" - of "int32_t": "I32" - of "int16_t": "I16" - of "int8_t": "I8" - of "uint64_t": "U64" - of "uint32_t": "U32" - of "uint16_t": "U16" - of "uint8_t": "U8" - of "bool": "Bool" - of "double": "F64" - of "float": "F32" - of "NimFfiStr": "Str" - of "NimFfiBytes": "Bytes" - else: cType +const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [ + skBool: ("bool", "bool"), + skI8: ("int8_t", "i8"), + skI16: ("int16_t", "i16"), + skI32: ("int32_t", "i32"), + skI64: ("int64_t", "i64"), + skU8: ("uint8_t", "u8"), + skU16: ("uint16_t", "u16"), + skU32: ("uint32_t", "u32"), + skU64: ("uint64_t", "u64"), + skF32: ("float", "f32"), + skF64: ("double", "f64"), +] func leafSuffix(cType: string): string = - ## Inverse of leafCType's cType→suffix for the leaf codecs the template - ## provides; empty string for composite types. + ## C type name → leaf codec suffix for the leaf codecs the template provides; + ## empty string for composite types. Driven off the shared scalar table so it + ## can't drift from the IR's scalar set. + for s in ScalarKind: + if scalarCInfoTable[s].cType == cType: + return scalarCInfoTable[s].suffix case cType - of "int64_t": "i64" - of "int32_t": "i32" - of "int16_t": "i16" - of "int8_t": "i8" - of "uint64_t": "u64" - of "uint32_t": "u32" - of "uint16_t": "u16" - of "uint8_t": "u8" - of "bool": "bool" - of "double": "f64" - of "float": "f32" of "NimFfiStr": "str" of "NimFfiBytes": "bytes" else: "" +func cToken(cType: string): string = + ## Short PascalCase token used to build monomorphised container names and + ## codec-adapter symbols. Leaf types reuse their codec suffix (e.g. + ## `int64_t`→`I64`); composite C type names are already unique C identifiers, + ## so they pass through verbatim. + let suffix = leafSuffix(cType) + if suffix.len > 0: + capitalizeFirstLetter(suffix) + else: + cType + type CTypeReg = object libName: string ## snake_case symbol prefix, e.g. "my_timer" libType: string ## PascalCase container-name prefix, e.g. "MyTimer" @@ -293,44 +255,48 @@ proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) = reg.codecs.add(body.join("\n")) reg.owns[t.name] = owns -proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] = - let t = nimType.strip() - if t.startsWith("ptr ") or t == "pointer": +proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool] = + ## Walks the shared type IR into a C type, monomorphising each distinct + ## `seq[T]` / `Option[T]` into its own struct + codec triple on first sight. + ## `owns` marks a C type that carries heap-allocated payload the caller must + ## release with its generated free function (strings, byte buffers, and any + ## seq/opt/struct transitively containing one); plain scalars and pointers own + ## nothing and need no cleanup. + case t.kind + of ftPtr: return (CPtrType, false) - let leaf = leafCType(t) - if leaf.ok: - return (leaf.cType, leaf.owns) - - let seqInner = genericInnerType(t, "seq[") - if seqInner.len > 0: - let inner = seqInner.strip() - if inner == "byte" or inner == "uint8": - return ("NimFfiBytes", true) - let (elemC, _) = ensureCType(reg, inner) + of ftScalar: + return (scalarCInfoTable[t.scalar].cType, false) + of ftStr: + return ("NimFfiStr", true) + of ftBytes: + return ("NimFfiBytes", true) + of ftSeq: + let (elemC, _) = ensureCType(reg, t.elem) let name = reg.libType & "Seq_" & cToken(elemC) if name notin reg.emitted: reg.emitted.incl(name) emitSeqType(reg, name, elemC) return (name, true) - - var optInner = genericInnerType(t, "Option[") - if optInner.len == 0: - optInner = genericInnerType(t, "Maybe[") - if optInner.len > 0: - let (elemC, elemOwns) = ensureCType(reg, optInner.strip()) + of ftOpt: + let (elemC, elemOwns) = ensureCType(reg, t.elem) let name = reg.libType & "Opt_" & cToken(elemC) if name notin reg.emitted: reg.emitted.incl(name) emitOptType(reg, name, elemC, elemOwns) return (name, reg.owns.getOrDefault(name, false)) + of ftStruct: + let name = t.name + if name notin reg.emitted: + reg.emitted.incl(name) + if name in reg.typeTable: + emitStructType(reg, reg.typeTable[name]) + else: + reg.decls.add("/* unknown type referenced: " & name & " */") + return (name, reg.owns.getOrDefault(name, false)) - if t notin reg.emitted: - reg.emitted.incl(t) - if t in reg.typeTable: - emitStructType(reg, reg.typeTable[t]) - else: - reg.decls.add("/* unknown type referenced: " & t & " */") - (t, reg.owns.getOrDefault(t, false)) +proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] = + ensureCType(reg, parseFFIType(nimType)) proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta = ## Synthesises the per-proc Req struct as an FFITypeMeta so it flows through @@ -344,10 +310,12 @@ proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta = func paramByValue(nimType: string, ridesAsPtr: bool): bool = ## Scalars / opaque pointers / string views pass by value; composite - ## aggregates (seq, Option, user structs) pass by const pointer. + ## aggregates (seq, Option, user structs) pass by const pointer. Note `ptr T` + ## rides by value as the 64-bit wire int (like `pointer`); production params + ## reach here as `pointer` since handles are pre-converted upstream. if ridesAsPtr: return true - leafCType(nimType.strip()).ok + parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr} proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string = if p.returnRidesAsPtr(): diff --git a/ffi/codegen/c_cpp_common.nim b/ffi/codegen/c_cpp_common.nim index f6357f5..fcabea0 100644 --- a/ffi/codegen/c_cpp_common.nim +++ b/ffi/codegen/c_cpp_common.nim @@ -6,16 +6,6 @@ import std/strutils import ./meta, ./string_helpers -proc genericInnerType*(typeName, prefix: string): string = - ## Inner type of a single-parameter generic written `Prefix[Inner]`, e.g. - ## `genericInnerType("seq[int]", "seq[")` → `"int"`. Empty string when - ## `typeName` is not of that shape. - if typeName.startsWith(prefix) and typeName.endsWith("]"): - let start = prefix.len - let lastIndex = typeName.len - 2 - return typeName[start .. lastIndex] - return "" - proc stripLibPrefix*(procName, libName: string): string = ## Drops the `_` prefix from an exported C symbol, e.g. ## `stripLibPrefix("timer_echo", "timer")` → `"echo"`. diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 8f0b382..dbf22dc 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -4,7 +4,7 @@ ## the Nim-side cbor_serial codec on the wire — both ends speak RFC 8949). import std/[os, strutils] -import ./meta, ./string_helpers, ./c_cpp_common +import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir ## Wire-format C++ type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so ## the CBOR payload size is stable regardless of host architecture. @@ -21,35 +21,37 @@ const ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl") CMakeListsTpl = staticRead("templates/cpp/CMakeLists.txt.tpl") +func cppScalar(s: ScalarKind): string = + case s + of skBool: "bool" + of skI8: "int8_t" + of skI16: "int16_t" + of skI32: "int32_t" + of skI64: "int64_t" + of skU8: "uint8_t" + of skU16: "uint16_t" + of skU32: "uint32_t" + of skU64: "uint64_t" + of skF32: "float" + of skF64: "double" + +func cppSeq(elem: string): string = + "std::vector<" & elem & ">" + +func cppOpt(elem: string): string = + "std::optional<" & elem & ">" + +const cppMap = NativeTypeMap( + scalar: cppScalar, + str: "std::string", + bytes: "std::vector", + ptrType: CppPtrType, + seqOf: cppSeq, + optOf: cppOpt, +) ## structName omitted: C++ uses the user type name verbatim + proc nimTypeToCpp*(typeName: string): string = - let trimmed = typeName.strip() - if trimmed.startsWith("ptr "): - return CppPtrType - else: - let seqInner = genericInnerType(trimmed, "seq[") - if seqInner.len > 0: - return "std::vector<" & nimTypeToCpp(seqInner) & ">" - let optionInner = genericInnerType(trimmed, "Option[") - if optionInner.len > 0: - return "std::optional<" & nimTypeToCpp(optionInner) & ">" - let maybeInner = genericInnerType(trimmed, "Maybe[") - if maybeInner.len > 0: - return "std::optional<" & nimTypeToCpp(maybeInner) & ">" - case trimmed - of "string", "cstring": "std::string" - of "int", "int64": "int64_t" - of "int32": "int32_t" - of "int16": "int16_t" - of "int8": "int8_t" - of "uint", "uint64": "uint64_t" - of "uint32": "uint32_t" - of "uint16": "uint16_t" - of "uint8", "byte": "uint8_t" - of "bool": "bool" - of "float", "float32": "float" - of "float64": "double" - of "pointer": CppPtrType - else: trimmed + renderNative(cppMap, parseFFIType(typeName)) proc emitStructCborCodec( lines: var seq[string], structName: string, fields: seq[(string, string)] diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 282911d..56544f3 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -1,38 +1,47 @@ ## Rust binding generator for the nim-ffi framework. ## Generates a complete Rust crate that uses CBOR (ciborium) on the wire. -import std/[os, strutils, sequtils] -import ./meta, ./string_helpers +import std/[os, strutils] +import ./meta, ./string_helpers, ./types_ir ## Wire-format Rust type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so ## the CBOR payload size is stable regardless of host architecture (mirrors ## CppPtrType in cpp.nim). const RustPtrType* = "u64" +func rustScalar(s: ScalarKind): string = + case s + of skBool: "bool" + of skI8: "i8" + of skI16: "i16" + of skI32: "i32" + of skI64: "i64" + of skU8: "u8" + of skU16: "u16" + of skU32: "u32" + of skU64: "u64" + of skF32: "f32" + of skF64: "f64" + +func rustSeq(elem: string): string = + "Vec<" & elem & ">" + +func rustOpt(elem: string): string = + "Option<" & elem & ">" + +const rustMap = NativeTypeMap( + scalar: rustScalar, + str: "String", + bytes: "Vec", + ptrType: RustPtrType, + seqOf: rustSeq, + optOf: rustOpt, + structName: capitalizeFirstLetter, +) + proc nimTypeToRust*(typeName: string): string = ## Maps Nim type names to Rust type names, including generics. - let t = typeName.strip() - if t.startsWith("seq[") and t.endsWith("]"): - return "Vec<" & nimTypeToRust(t[4 .. ^2]) & ">" - if t.startsWith("Option[") and t.endsWith("]"): - return "Option<" & nimTypeToRust(t[7 .. ^2]) & ">" - if t.startsWith("Maybe[") and t.endsWith("]"): - return "Option<" & nimTypeToRust(t[6 .. ^2]) & ">" - case t - of "string", "cstring": - "String" - of "int", "int64": - "i64" - of "int32": - "i32" - of "bool": - "bool" - of "float", "float64": - "f64" - of "pointer": - RustPtrType - else: - capitalizeFirstLetter(t) + renderNative(rustMap, parseFFIType(typeName)) proc deriveLibName*(procs: seq[FFIProcMeta]): string = ## Extracts the common prefix before the first `_` from proc names. diff --git a/ffi/codegen/string_helpers.nim b/ffi/codegen/string_helpers.nim index fbe73aa..1834a01 100644 --- a/ffi/codegen/string_helpers.nim +++ b/ffi/codegen/string_helpers.nim @@ -25,7 +25,7 @@ proc camelToSnakeCase*(s: string): string = first = false return snake -proc capitalizeFirstLetter*(s: string): string = +func capitalizeFirstLetter*(s: string): string = ## Returns `s` with its first rune uppercased; the rest is left unchanged. ## e.g. "abc" → "Abc", "" → "", "Abc" → "Abc" if s.len == 0: diff --git a/ffi/codegen/types_ir.nim b/ffi/codegen/types_ir.nim new file mode 100644 index 0000000..5771524 --- /dev/null +++ b/ffi/codegen/types_ir.nim @@ -0,0 +1,134 @@ +## Structured type model shared by the C / C++ / Rust binding generators: one +## parser (`parseFFIType`) for the Nim type strings each backend used to slice +## by hand, plus `renderNative` to walk the result into a backend's type string. + +import std/[strutils, options] + +type + ScalarKind* {.pure.} = enum + skBool + skI8 + skI16 + skI32 + skI64 + skU8 + skU16 + skU32 + skU64 + skF32 + skF64 + + FFITypeKind* {.pure.} = enum + ftScalar + ftStr + ftBytes + ftSeq + ftOpt + ftPtr + ftStruct + + FFIType* = ref object + case kind*: FFITypeKind + of ftScalar: + scalar*: ScalarKind + of ftSeq, ftOpt: + elem*: FFIType + of ftStruct: + name*: string + else: + discard + + NativeTypeMap* = object + ## A backend's answer to "what do you call this kind?". `seqOf`/`optOf` + ## wrap an already-rendered element; `structName` maps a user type name. + scalar*: proc(s: ScalarKind): string {.noSideEffect, nimcall.} + str*: string + bytes*: string + ptrType*: string + seqOf*: proc(elem: string): string {.noSideEffect, nimcall.} + optOf*: proc(elem: string): string {.noSideEffect, nimcall.} + structName*: proc(name: string): string {.noSideEffect, nimcall.} + ## nil ⇒ the user type name passes through unchanged + +func genericInnerType(typeName, prefix: string): string = + ## Inner type of a single-parameter generic `Prefix[Inner]`, e.g. + ## `genericInnerType("seq[int]", "seq[")` → `"int"`; "" if not that shape. + if typeName.startsWith(prefix) and typeName.endsWith("]"): + return typeName[prefix.len .. ^2] + return "" + +func scalarKind(t: string): Option[ScalarKind] = + ## Single source of truth for the scalar leaf set every backend shares. + case t + of "bool": + some(skBool) + of "int8": + some(skI8) + of "int16": + some(skI16) + of "int32": + some(skI32) + of "int", "int64": + some(skI64) + of "uint8", "byte": + some(skU8) + of "uint16": + some(skU16) + of "uint32": + some(skU32) + of "uint", "uint64": + some(skU64) + of "float32": + some(skF32) + of "float", "float64": + some(skF64) + else: + none(ScalarKind) + +func parseFFIType*(typeName: string): FFIType = + ## Single source of truth for turning a Nim type string into the shared IR: + ## ptr/pointer, seq[byte]→bytes, seq/Option/Maybe, scalars, string, else struct. + let t = typeName.strip() + if t.startsWith("ptr ") or t == "pointer": + return FFIType(kind: ftPtr) + + let seqInner = genericInnerType(t, "seq[") + if seqInner.len > 0: + let inner = seqInner.strip() + if inner == "byte" or inner == "uint8": + return FFIType(kind: ftBytes) + return FFIType(kind: ftSeq, elem: parseFFIType(inner)) + + var optInner = genericInnerType(t, "Option[") + if optInner.len == 0: + optInner = genericInnerType(t, "Maybe[") + if optInner.len > 0: + return FFIType(kind: ftOpt, elem: parseFFIType(optInner.strip())) + + let sc = scalarKind(t) + if sc.isSome(): + return FFIType(kind: ftScalar, scalar: sc.get()) + if t == "string" or t == "cstring": + return FFIType(kind: ftStr) + FFIType(kind: ftStruct, name: t) + +func renderNative*(m: NativeTypeMap, t: FFIType): string = + ## Recursively walks `t` into a native type string for the backend `m`. + case t.kind + of ftScalar: + m.scalar(t.scalar) + of ftStr: + m.str + of ftBytes: + m.bytes + of ftPtr: + m.ptrType + of ftSeq: + m.seqOf(renderNative(m, t.elem)) + of ftOpt: + m.optOf(renderNative(m, t.elem)) + of ftStruct: + if m.structName.isNil(): + t.name + else: + m.structName(t.name) diff --git a/tests/unit/test_rust_codegen.nim b/tests/unit/test_rust_codegen.nim new file mode 100644 index 0000000..e2caa79 --- /dev/null +++ b/tests/unit/test_rust_codegen.nim @@ -0,0 +1,43 @@ +## Regression tests for the Rust type mapping. `nimTypeToRust` used to carry its +## own scalar table that had drifted from C/C++ — `int8`/`int16`/`uint8`/ +## `uint16`/`uint32`/`byte`/`float32` fell through to `capitalizeFirstLetter` +## and emitted invalid Rust. It now renders through the shared `parseFFIType` +## IR, so the full scalar set is pinned here. + +import unittest2 +import ffi/codegen/rust + +suite "nimTypeToRust: scalar set": + test "every scalar maps to its Rust primitive (the drift that regressed)": + check nimTypeToRust("bool") == "bool" + check nimTypeToRust("int8") == "i8" + check nimTypeToRust("int16") == "i16" + check nimTypeToRust("int32") == "i32" + check nimTypeToRust("int") == "i64" + check nimTypeToRust("int64") == "i64" + check nimTypeToRust("uint8") == "u8" + check nimTypeToRust("byte") == "u8" + check nimTypeToRust("uint16") == "u16" + check nimTypeToRust("uint32") == "u32" + check nimTypeToRust("uint") == "u64" + check nimTypeToRust("uint64") == "u64" + check nimTypeToRust("float32") == "f32" + check nimTypeToRust("float") == "f64" + check nimTypeToRust("float64") == "f64" + +suite "nimTypeToRust: strings, pointers and containers": + test "string types render to String": + check nimTypeToRust("string") == "String" + check nimTypeToRust("cstring") == "String" + + test "seq[byte] collapses to Vec and ptr/pointer to the wire int": + check nimTypeToRust("seq[byte]") == "Vec" + check nimTypeToRust("ptr Foo") == RustPtrType + check nimTypeToRust("pointer") == RustPtrType + + test "generics nest and Maybe aliases Option": + check nimTypeToRust("seq[Option[int8]]") == "Vec>" + check nimTypeToRust("Maybe[uint16]") == "Option" + + test "an unknown user type is capitalised, not mistaken for a scalar": + check nimTypeToRust("echoRequest") == "EchoRequest" diff --git a/tests/unit/test_types_ir.nim b/tests/unit/test_types_ir.nim new file mode 100644 index 0000000..dab7699 --- /dev/null +++ b/tests/unit/test_types_ir.nim @@ -0,0 +1,122 @@ +## Unit tests for the shared type IR that the C / C++ / Rust binding generators +## parse Nim type strings through. `parseFFIType` is the single source of truth +## the three backends consume, so its shape mappings are pinned here directly. + +import unittest2 +import ffi/codegen/types_ir + +suite "parseFFIType: scalars": + test "the full scalar set round-trips to its ScalarKind": + let cases = { + "bool": skBool, + "int8": skI8, + "int16": skI16, + "int32": skI32, + "int": skI64, + "int64": skI64, + "uint8": skU8, + "byte": skU8, + "uint16": skU16, + "uint32": skU32, + "uint": skU64, + "uint64": skU64, + "float32": skF32, + "float": skF64, + "float64": skF64, + } + for (name, kind) in cases: + let t = parseFFIType(name) + check t.kind == ftScalar + check t.scalar == kind + + test "surrounding whitespace is stripped": + let t = parseFFIType(" int ") + check t.kind == ftScalar + check t.scalar == skI64 + +suite "parseFFIType: strings, bytes and pointers": + test "string and cstring are ftStr": + check parseFFIType("string").kind == ftStr + check parseFFIType("cstring").kind == ftStr + + test "seq[byte] and seq[uint8] collapse to ftBytes": + check parseFFIType("seq[byte]").kind == ftBytes + check parseFFIType("seq[uint8]").kind == ftBytes + + test "ptr T and pointer are ftPtr": + check parseFFIType("ptr Foo").kind == ftPtr + check parseFFIType("pointer").kind == ftPtr + +suite "parseFFIType: containers": + test "seq[T] wraps the parsed element": + let t = parseFFIType("seq[int]") + check t.kind == ftSeq + check t.elem.kind == ftScalar + check t.elem.scalar == skI64 + + test "Maybe[T] is the same as Option[T]": + let opt = parseFFIType("Option[string]") + let maybe = parseFFIType("Maybe[string]") + check opt.kind == ftOpt + check maybe.kind == ftOpt + check opt.elem.kind == ftStr + check maybe.elem.kind == ftStr + + test "nested containers parse all the way down": + let t = parseFFIType("seq[Option[seq[int]]]") + check t.kind == ftSeq + check t.elem.kind == ftOpt + check t.elem.elem.kind == ftSeq + check t.elem.elem.elem.kind == ftScalar + check t.elem.elem.elem.scalar == skI64 + + test "seq[Option[byte]] keeps the inner byte as a scalar, not bytes": + let t = parseFFIType("seq[Option[byte]]") + check t.kind == ftSeq + check t.elem.kind == ftOpt + check t.elem.elem.kind == ftScalar + check t.elem.elem.scalar == skU8 + +suite "parseFFIType: structs": + test "an unknown name is an ftStruct carrying the name": + let t = parseFFIType("EchoRequest") + check t.kind == ftStruct + check t.name == "EchoRequest" + +suite "renderNative: walks the IR with a backend map": + let rustish = NativeTypeMap( + scalar: proc(s: ScalarKind): string = + ( + case s + of skI64: "i64" + of skU8: "u8" + else: "?" + ), + str: "String", + bytes: "Vec", + ptrType: "u64", + seqOf: proc(e: string): string = + "Vec<" & e & ">", + optOf: proc(e: string): string = + "Option<" & e & ">", + structName: proc(n: string): string = + n, + ) + + test "scalars, strings, bytes and pointers map to the leaf entries": + check renderNative(rustish, parseFFIType("int")) == "i64" + check renderNative(rustish, parseFFIType("string")) == "String" + check renderNative(rustish, parseFFIType("seq[byte]")) == "Vec" + check renderNative(rustish, parseFFIType("ptr Foo")) == "u64" + + test "nested containers render recursively": + check renderNative(rustish, parseFFIType("seq[Option[int]]")) == "Vec>" + + test "a struct renders through structName when set": + check renderNative(rustish, parseFFIType("EchoRequest")) == "EchoRequest" + + test "a nil structName passes the user type name through unchanged": + let noStructMap = NativeTypeMap( + scalar: rustish.scalar, str: "String", bytes: "Vec", ptrType: "u64" + ) + check renderNative(noStructMap, parseFFIType("EchoRequest")) == "EchoRequest"