@thi.ng/wasm-api-bindgen
Advanced tools
Comparing version 0.4.32 to 0.4.33
29
api.js
@@ -1,17 +0,16 @@ | ||
export const PKG_NAME = "@thi.ng/wasm-api-bindgen"; | ||
/** | ||
* WASM32 target spec | ||
*/ | ||
export const WASM32 = { | ||
isize: "i32", | ||
usize: "u32", | ||
sizeBytes: 4, | ||
const PKG_NAME = "@thi.ng/wasm-api-bindgen"; | ||
const WASM32 = { | ||
isize: "i32", | ||
usize: "u32", | ||
sizeBytes: 4 | ||
}; | ||
/** | ||
* WASM64 target spec | ||
*/ | ||
export const WASM64 = { | ||
isize: "i64", | ||
usize: "u64", | ||
sizeBytes: 8, | ||
const WASM64 = { | ||
isize: "i64", | ||
usize: "u64", | ||
sizeBytes: 8 | ||
}; | ||
export { | ||
PKG_NAME, | ||
WASM32, | ||
WASM64 | ||
}; |
575
c11.js
@@ -6,313 +6,300 @@ import { topoSort } from "@thi.ng/arrays/topo-sort"; | ||
import { classifyField } from "./internal/classify.js"; | ||
import { ensureStringArray, enumName, isOpaque, isPadding, isStringSlice, isStruct, isUnion, isWasmString, prefixLines, sliceTypes, withIndentation, } from "./internal/utils.js"; | ||
import { | ||
ensureStringArray, | ||
enumName, | ||
isOpaque, | ||
isPadding, | ||
isStringSlice, | ||
isStruct, | ||
isUnion, | ||
isWasmString, | ||
prefixLines, | ||
sliceTypes, | ||
withIndentation | ||
} from "./internal/utils.js"; | ||
const PRIM_ALIASES = { | ||
i8: "int8_t", | ||
u8: "uint8_t", | ||
i16: "int16_t", | ||
u16: "uint16_t", | ||
i32: "int32_t", | ||
u32: "uint32_t", | ||
i64: "int64_t", | ||
u64: "uint64_t", | ||
f32: "float", | ||
f64: "double", | ||
i8: "int8_t", | ||
u8: "uint8_t", | ||
i16: "int16_t", | ||
u16: "uint16_t", | ||
i32: "int32_t", | ||
u32: "uint32_t", | ||
i64: "int64_t", | ||
u64: "uint64_t", | ||
f32: "float", | ||
f64: "double" | ||
}; | ||
/** | ||
* Zig code generator. Call with options and then pass to {@link generateTypes} | ||
* (see its docs for further usage). | ||
* | ||
* @remarks | ||
* This codegen generates struct and enum definitions for a {@link TypeColl} | ||
* given to {@link generateTypes}. | ||
* | ||
* @param opts | ||
*/ | ||
export const C11 = (opts = {}) => { | ||
const { typePrefix } = { | ||
typePrefix: "", | ||
...opts, | ||
}; | ||
const INDENT = " "; | ||
const SCOPES = [/\{$/, /^\}[ A-Za-z0-9_]*[;,]?$/]; | ||
const gen = { | ||
id: "c", | ||
pre: (coll, opts) => { | ||
const res = [ | ||
"#pragma once", | ||
"", | ||
"#ifdef __cplusplus", | ||
`extern "C" {`, | ||
"#endif", | ||
opts.debug ? "\n#include <stdalign.h>" : "", | ||
`#include "wasmapi.h"`, | ||
"", | ||
]; | ||
// pre-declare any primitive slice types used (e.g. `U8Slice`) | ||
const slices = sliceTypes(coll); | ||
for (let id of slices) { | ||
const prim = PRIM_ALIASES[id]; | ||
if (!prim) | ||
continue; | ||
res.push(__sliceDef(prim, typePrefix, capitalize(id))); | ||
} | ||
// ...then pre-declare any custom types in dependency order | ||
for (let id of __declOrder(coll)) { | ||
const type = coll[id]; | ||
if (type.type == "funcptr") { | ||
res.push(__funcptr(type, coll, opts, typePrefix)); | ||
} | ||
else { | ||
res.push(`\ntypedef ${type.type} ${typePrefix}${type.name} ${typePrefix}${type.name};`); | ||
} | ||
if (slices.has(id)) { | ||
const [ptr, name] = id === "opaque" | ||
? ["void*", "Opaque"] | ||
: [typePrefix + id, capitalize(id)]; | ||
res.push(__sliceDef(ptr, typePrefix, name)); | ||
} | ||
} | ||
if (opts.pre) | ||
res.push("", ...ensureStringArray(opts.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => { | ||
const res = []; | ||
if (opts.post) | ||
res.push(...ensureStringArray(opts.post), ""); | ||
res.push("#ifdef __cplusplus", "}", "#endif", ""); | ||
return res.join("\n"); | ||
}, | ||
doc: (doc, acc, opts) => { | ||
acc.push(...prefixLines("// ", doc, opts.lineWidth)); | ||
}, | ||
enum: (e, _, acc, opts) => { | ||
if (!(e.tag === "i32" || e.tag === "u32")) { | ||
unsupported(`enum ${e.name} must be a i32/u32 in C, but got '${e.tag}'`); | ||
} | ||
const name = typePrefix + e.name; | ||
const lines = []; | ||
lines.push(`enum ${name} {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, lines, opts); | ||
line = enumName(opts, v.name); | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} | ||
else { | ||
line = enumName(opts, v); | ||
} | ||
lines.push(line + ","); | ||
} | ||
lines.push(`};`, ""); | ||
acc.push(...withIndentation(lines, INDENT, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts) => { | ||
const name = typePrefix + struct.name; | ||
acc.push(...withIndentation([ | ||
`struct ${name} {`, | ||
...__generateFields(gen, struct, coll, opts, typePrefix), | ||
], INDENT, ...SCOPES)); | ||
}, | ||
union: (union, coll, acc, opts) => { | ||
const name = typePrefix + union.name; | ||
acc.push(...withIndentation([ | ||
`union ${name} {`, | ||
...__generateFields(gen, union, coll, opts, typePrefix), | ||
], INDENT, ...SCOPES)); | ||
}, | ||
// funcpointers are emitted in `pre` phase above | ||
funcptr: () => { }, | ||
}; | ||
return gen; | ||
}; | ||
/** | ||
* Generates source code for given {@link Struct} or {@link Union}. | ||
* | ||
* @internal | ||
*/ | ||
const __generateFields = (gen, parent, coll, opts, typePrefix) => { | ||
const res = []; | ||
const ftypes = {}; | ||
const isUnion = parent.type === "union"; | ||
const name = typePrefix + parent.name; | ||
let padID = 0; | ||
for (let f of parent.fields) { | ||
// autolabel explicit padding fields | ||
if (isPadding(f)) { | ||
res.push(`uint8_t __pad${padID++}[${f.pad}];`); | ||
continue; | ||
const C11 = (opts = {}) => { | ||
const { typePrefix } = { | ||
typePrefix: "", | ||
...opts | ||
}; | ||
const INDENT = " "; | ||
const SCOPES = [/\{$/, /^\}[ A-Za-z0-9_]*[;,]?$/]; | ||
const gen = { | ||
id: "c", | ||
pre: (coll, opts2) => { | ||
const res = [ | ||
"#pragma once", | ||
"", | ||
"#ifdef __cplusplus", | ||
`extern "C" {`, | ||
"#endif", | ||
opts2.debug ? "\n#include <stdalign.h>" : "", | ||
`#include "wasmapi.h"`, | ||
"" | ||
]; | ||
const slices = sliceTypes(coll); | ||
for (let id of slices) { | ||
const prim = PRIM_ALIASES[id]; | ||
if (!prim) | ||
continue; | ||
res.push(__sliceDef(prim, typePrefix, capitalize(id))); | ||
} | ||
for (let id of __declOrder(coll)) { | ||
const type = coll[id]; | ||
if (type.type == "funcptr") { | ||
res.push( | ||
__funcptr(type, coll, opts2, typePrefix) | ||
); | ||
} else { | ||
res.push( | ||
` | ||
typedef ${type.type} ${typePrefix}${type.name} ${typePrefix}${type.name};` | ||
); | ||
} | ||
f.doc && gen.doc(f.doc, res, opts); | ||
const { type, decl, sentinel } = __fieldType(f, coll, opts, typePrefix); | ||
ftypes[f.name] = type; | ||
res.push(decl + ";"); | ||
if (sentinel) { | ||
res.push(`// Hidden sentinel. Must be manually initialized to ${f.sentinel || 0}`, `${sentinel} __${f.name}Sentinel;`); | ||
if (slices.has(id)) { | ||
const [ptr, name] = id === "opaque" ? ["void*", "Opaque"] : [typePrefix + id, capitalize(id)]; | ||
res.push(__sliceDef(ptr, typePrefix, name)); | ||
} | ||
} | ||
res.push("};"); | ||
if (opts.debug) { | ||
const fn = (fname, body) => res.push("", `size_t __attribute__((used)) ${name}_${fname}() {`, `return ${body};`, `}`); | ||
fn("align", `alignof(${name})`); | ||
fn("size", `sizeof(${name})`); | ||
for (let f of parent.fields) { | ||
if (isPadding(f)) | ||
continue; | ||
fn(f.name + "_align", `alignof(${ftypes[f.name]})`); | ||
!isUnion && fn(f.name + "_offset", `offsetof(${name}, ${f.name})`); | ||
fn(f.name + "_size", `sizeof(${ftypes[f.name]})`); | ||
} | ||
if (opts2.pre) | ||
res.push("", ...ensureStringArray(opts2.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => { | ||
const res = []; | ||
if (opts.post) | ||
res.push(...ensureStringArray(opts.post), ""); | ||
res.push("#ifdef __cplusplus", "}", "#endif", ""); | ||
return res.join("\n"); | ||
}, | ||
doc: (doc, acc, opts2) => { | ||
acc.push(...prefixLines("// ", doc, opts2.lineWidth)); | ||
}, | ||
enum: (e, _, acc, opts2) => { | ||
if (!(e.tag === "i32" || e.tag === "u32")) { | ||
unsupported( | ||
`enum ${e.name} must be a i32/u32 in C, but got '${e.tag}'` | ||
); | ||
} | ||
const name = typePrefix + e.name; | ||
const lines = []; | ||
lines.push(`enum ${name} {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, lines, opts2); | ||
line = enumName(opts2, v.name); | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} else { | ||
line = enumName(opts2, v); | ||
} | ||
lines.push(line + ","); | ||
} | ||
lines.push(`};`, ""); | ||
acc.push(...withIndentation(lines, INDENT, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts2) => { | ||
const name = typePrefix + struct.name; | ||
acc.push( | ||
...withIndentation( | ||
[ | ||
`struct ${name} {`, | ||
...__generateFields( | ||
gen, | ||
struct, | ||
coll, | ||
opts2, | ||
typePrefix | ||
) | ||
], | ||
INDENT, | ||
...SCOPES | ||
) | ||
); | ||
}, | ||
union: (union, coll, acc, opts2) => { | ||
const name = typePrefix + union.name; | ||
acc.push( | ||
...withIndentation( | ||
[ | ||
`union ${name} {`, | ||
...__generateFields(gen, union, coll, opts2, typePrefix) | ||
], | ||
INDENT, | ||
...SCOPES | ||
) | ||
); | ||
}, | ||
// funcpointers are emitted in `pre` phase above | ||
funcptr: () => { | ||
} | ||
res.push(""); | ||
return res; | ||
}; | ||
return gen; | ||
}; | ||
/** | ||
* Returns the C type name and declaration (incl. field name) for a given | ||
* {@link Field}. | ||
* | ||
* @internal | ||
*/ | ||
const __generateFields = (gen, parent, coll, opts, typePrefix) => { | ||
const res = []; | ||
const ftypes = {}; | ||
const isUnion2 = parent.type === "union"; | ||
const name = typePrefix + parent.name; | ||
let padID = 0; | ||
for (let f of parent.fields) { | ||
if (isPadding(f)) { | ||
res.push(`uint8_t __pad${padID++}[${f.pad}];`); | ||
continue; | ||
} | ||
f.doc && gen.doc(f.doc, res, opts); | ||
const { type, decl, sentinel } = __fieldType(f, coll, opts, typePrefix); | ||
ftypes[f.name] = type; | ||
res.push(decl + ";"); | ||
if (sentinel) { | ||
res.push( | ||
`// Hidden sentinel. Must be manually initialized to ${f.sentinel || 0}`, | ||
`${sentinel} __${f.name}Sentinel;` | ||
); | ||
} | ||
} | ||
res.push("};"); | ||
if (opts.debug) { | ||
const fn = (fname, body) => res.push( | ||
"", | ||
`size_t __attribute__((used)) ${name}_${fname}() {`, | ||
`return ${body};`, | ||
`}` | ||
); | ||
fn("align", `alignof(${name})`); | ||
fn("size", `sizeof(${name})`); | ||
for (let f of parent.fields) { | ||
if (isPadding(f)) | ||
continue; | ||
fn(f.name + "_align", `alignof(${ftypes[f.name]})`); | ||
!isUnion2 && fn(f.name + "_offset", `offsetof(${name}, ${f.name})`); | ||
fn(f.name + "_size", `sizeof(${ftypes[f.name]})`); | ||
} | ||
} | ||
res.push(""); | ||
return res; | ||
}; | ||
const __fieldType = (f, coll, opts, prefix) => { | ||
let type = f.type; | ||
let decl; | ||
let sentinel; | ||
const { classifier, isConst } = classifyField(f, coll); | ||
const $isConst = isConst ? "Const" : ""; | ||
const __ptr = () => { | ||
decl = `${type}* ${f.name}`; | ||
type = `${type}*`; | ||
}; | ||
const __array = () => { | ||
decl = `${type} ${f.name}[${f.len}]`; | ||
type = `${type}[${f.len}]`; | ||
}; | ||
const __slice = () => { | ||
type += "Slice"; | ||
let type = f.type; | ||
let decl; | ||
let sentinel; | ||
const { classifier, isConst } = classifyField(f, coll); | ||
const $isConst = isConst ? "Const" : ""; | ||
const __ptr = () => { | ||
decl = `${type}* ${f.name}`; | ||
type = `${type}*`; | ||
}; | ||
const __array = () => { | ||
decl = `${type} ${f.name}[${f.len}]`; | ||
type = `${type}[${f.len}]`; | ||
}; | ||
const __slice = () => { | ||
type += "Slice"; | ||
decl = `${type} ${f.name}`; | ||
}; | ||
if (isWasmString(f.type)) { | ||
const useStrSlice = isStringSlice(opts.stringType); | ||
type = prefix + (useStrSlice ? `${$isConst}String` : `${$isConst}StringPtr`); | ||
switch (classifier) { | ||
case "strPtr": | ||
case "strPtrFixed": | ||
case "strPtrMulti": | ||
__ptr(); | ||
break; | ||
case "strSlice": | ||
__slice(); | ||
break; | ||
case "strArray": | ||
__array(); | ||
break; | ||
default: | ||
decl = `${type} ${f.name}`; | ||
}; | ||
if (isWasmString(f.type)) { | ||
const useStrSlice = isStringSlice(opts.stringType); | ||
type = | ||
prefix + | ||
(useStrSlice ? `${$isConst}String` : `${$isConst}StringPtr`); | ||
switch (classifier) { | ||
case "strPtr": | ||
case "strPtrFixed": | ||
case "strPtrMulti": | ||
__ptr(); | ||
break; | ||
case "strSlice": | ||
__slice(); | ||
break; | ||
case "strArray": | ||
__array(); | ||
break; | ||
default: | ||
decl = `${type} ${f.name}`; | ||
} | ||
} | ||
else if (isOpaque(f.type)) { | ||
type = `${prefix}${$isConst}OpaquePtr`; | ||
switch (classifier) { | ||
case "opaquePtr": | ||
case "opaquePtrFixed": | ||
case "opaquePtrMulti": | ||
__ptr(); | ||
break; | ||
case "opaqueSlice": | ||
__slice(); | ||
break; | ||
case "opaqueArray": | ||
__array(); | ||
break; | ||
default: | ||
decl = `${type} ${f.name}`; | ||
} | ||
} else if (isOpaque(f.type)) { | ||
type = `${prefix}${$isConst}OpaquePtr`; | ||
switch (classifier) { | ||
case "opaquePtr": | ||
case "opaquePtrFixed": | ||
case "opaquePtrMulti": | ||
__ptr(); | ||
break; | ||
case "opaqueSlice": | ||
__slice(); | ||
break; | ||
case "opaqueArray": | ||
__array(); | ||
break; | ||
default: | ||
decl = `${type} ${f.name}`; | ||
} | ||
else { | ||
const $const = isConst ? "const " : ""; | ||
type = PRIM_ALIASES[type] || prefix + type; | ||
switch (classifier) { | ||
case "ptr": | ||
case "ptrFixed": | ||
case "ptrMulti": | ||
case "enumPtr": | ||
case "enumPtrFixed": | ||
case "enumPtrMulti": | ||
type = `${$const}${type}*`; | ||
decl = `${type} ${f.name}`; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
type = `${prefix}${$isConst}${capitalize(f.type)}Slice`; | ||
decl = `${type} ${f.name}`; | ||
break; | ||
case "array": | ||
case "enumArray": | ||
__array(); | ||
if (f.sentinel != null) { | ||
sentinel = PRIM_ALIASES[f.type]; | ||
} | ||
break; | ||
case "vec": | ||
unsupported("C doesn't support vector"); | ||
default: | ||
decl = `${type} ${f.name}`; | ||
} else { | ||
const $const = isConst ? "const " : ""; | ||
type = PRIM_ALIASES[type] || prefix + type; | ||
switch (classifier) { | ||
case "ptr": | ||
case "ptrFixed": | ||
case "ptrMulti": | ||
case "enumPtr": | ||
case "enumPtrFixed": | ||
case "enumPtrMulti": | ||
type = `${$const}${type}*`; | ||
decl = `${type} ${f.name}`; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
type = `${prefix}${$isConst}${capitalize(f.type)}Slice`; | ||
decl = `${type} ${f.name}`; | ||
break; | ||
case "array": | ||
case "enumArray": | ||
__array(); | ||
if (f.sentinel != null) { | ||
sentinel = PRIM_ALIASES[f.type]; | ||
} | ||
break; | ||
case "vec": | ||
unsupported("C doesn't support vector"); | ||
default: | ||
decl = `${type} ${f.name}`; | ||
} | ||
return { | ||
type: type, | ||
decl: decl, | ||
sentinel: sentinel, | ||
}; | ||
} | ||
return { | ||
type, | ||
decl, | ||
sentinel | ||
}; | ||
}; | ||
/** | ||
* Takes a {@link TypeColl} and returns user type names in dependency order. | ||
* | ||
* @param coll | ||
* | ||
* @internal | ||
*/ | ||
const __declOrder = (coll) => topoSort(coll, (type) => { | ||
const fields = isStruct(type) || isUnion(type) | ||
? type.fields | ||
: type.type === "funcptr" | ||
? type.args | ||
: undefined; | ||
return fields | ||
? fields.map((x) => x.type).filter((x) => !!coll[x]) | ||
: undefined; | ||
const fields = isStruct(type) || isUnion(type) ? type.fields : type.type === "funcptr" ? type.args : void 0; | ||
return fields ? fields.map((x) => x.type).filter((x) => !!coll[x]) : void 0; | ||
}); | ||
/** | ||
* Generates C `typedef` for given {@link FuncPointer} spec. | ||
* | ||
* @param ptr | ||
* @param coll | ||
* @param opts | ||
* @param typePrefix | ||
* | ||
* @internal | ||
*/ | ||
const __funcptr = (ptr, coll, opts, typePrefix) => { | ||
const name = typePrefix + ptr.name; | ||
const args = ptr.args | ||
.map((a) => __fieldType(a, coll, opts, typePrefix).decl) | ||
.join(", "); | ||
const rtype = ptr.rtype === "void" | ||
? ptr.rtype | ||
: __fieldType({ name: "return", ...ptr.rtype }, coll, opts, typePrefix).type; | ||
return `typedef ${rtype} (*${name})(${args});`; | ||
const name = typePrefix + ptr.name; | ||
const args = ptr.args.map((a) => __fieldType(a, coll, opts, typePrefix).decl).join(", "); | ||
const rtype = ptr.rtype === "void" ? ptr.rtype : __fieldType( | ||
{ name: "return", ...ptr.rtype }, | ||
coll, | ||
opts, | ||
typePrefix | ||
).type; | ||
return `typedef ${rtype} (*${name})(${args});`; | ||
}; | ||
/** | ||
* Generates `typedef`s for a slice emulation struct of given pointer type. | ||
* | ||
* @param ptr | ||
* @param prefix | ||
* @param name | ||
* | ||
* @internal | ||
*/ | ||
const __sliceDef = (ptr, prefix, name) => `\ntypedef struct { ${ptr}* ptr; size_t len; } ${prefix}${name}Slice;` + | ||
`\ntypedef struct { const ${ptr}* ptr; size_t len; } ${prefix}Const${name}Slice;`; | ||
const __sliceDef = (ptr, prefix, name) => ` | ||
typedef struct { ${ptr}* ptr; size_t len; } ${prefix}${name}Slice; | ||
typedef struct { const ${ptr}* ptr; size_t len; } ${prefix}Const${name}Slice;`; | ||
export { | ||
C11 | ||
}; |
# Change Log | ||
- **Last updated**: 2023-12-09T19:12:04Z | ||
- **Last updated**: 2023-12-11T10:07:09Z | ||
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub) | ||
@@ -5,0 +5,0 @@ |
333
cli.js
@@ -1,2 +0,11 @@ | ||
import { ParseError, flag, oneOf, oneOfMulti, parse, string, strings, usage, } from "@thi.ng/args"; | ||
import { | ||
ParseError, | ||
flag, | ||
oneOf, | ||
oneOfMulti, | ||
parse, | ||
string, | ||
strings, | ||
usage | ||
} from "@thi.ng/args"; | ||
import { isArray, isPlainObject, isString } from "@thi.ng/checks"; | ||
@@ -10,3 +19,9 @@ import { illegalArgs } from "@thi.ng/errors"; | ||
import { generateTypes } from "./codegen.js"; | ||
import { isOpaque, isPadding, isSizeT, isWasmPrim, isWasmString, } from "./internal/utils.js"; | ||
import { | ||
isOpaque, | ||
isPadding, | ||
isSizeT, | ||
isWasmPrim, | ||
isWasmString | ||
} from "./internal/utils.js"; | ||
import { TYPESCRIPT } from "./typescript.js"; | ||
@@ -16,48 +31,48 @@ import { ZIG } from "./zig.js"; | ||
const argOpts = { | ||
analytics: string({ | ||
alias: "a", | ||
hint: "FILE", | ||
desc: "output file path for raw codegen analytics", | ||
}), | ||
config: string({ | ||
alias: "c", | ||
hint: "FILE", | ||
desc: "JSON config file with codegen options", | ||
}), | ||
debug: flag({ | ||
alias: "d", | ||
default: false, | ||
desc: "enable debug output & functions", | ||
}), | ||
dryRun: flag({ | ||
default: false, | ||
desc: "enable dry run (don't overwrite files)", | ||
}), | ||
lang: oneOfMulti(Object.keys(GENERATORS), { | ||
alias: "l", | ||
desc: "target language", | ||
default: ["ts", "zig"], | ||
delim: ",", | ||
}), | ||
out: strings({ alias: "o", hint: "FILE", desc: "output file path" }), | ||
string: oneOf(["slice", "ptr"], { | ||
alias: "s", | ||
hint: "TYPE", | ||
desc: "Force string type implementation", | ||
}), | ||
analytics: string({ | ||
alias: "a", | ||
hint: "FILE", | ||
desc: "output file path for raw codegen analytics" | ||
}), | ||
config: string({ | ||
alias: "c", | ||
hint: "FILE", | ||
desc: "JSON config file with codegen options" | ||
}), | ||
debug: flag({ | ||
alias: "d", | ||
default: false, | ||
desc: "enable debug output & functions" | ||
}), | ||
dryRun: flag({ | ||
default: false, | ||
desc: "enable dry run (don't overwrite files)" | ||
}), | ||
lang: oneOfMulti(Object.keys(GENERATORS), { | ||
alias: "l", | ||
desc: "target language", | ||
default: ["ts", "zig"], | ||
delim: "," | ||
}), | ||
out: strings({ alias: "o", hint: "FILE", desc: "output file path" }), | ||
string: oneOf(["slice", "ptr"], { | ||
alias: "s", | ||
hint: "TYPE", | ||
desc: "Force string type implementation" | ||
}) | ||
}; | ||
export const INSTALL_DIR = resolve(`${process.argv[2]}/..`); | ||
export const PKG = readJSON(`${INSTALL_DIR}/package.json`); | ||
export const APP_NAME = PKG.name.split("/")[1]; | ||
export const HEADER = ` | ||
█ █ █ │ | ||
██ █ │ | ||
█ █ █ █ █ █ █ █ │ ${PKG.name} ${PKG.version} | ||
█ █ █ █ █ █ █ █ █ │ Multi-language data bindings code generator | ||
█ │ | ||
█ █ │ | ||
const INSTALL_DIR = resolve(`${process.argv[2]}/..`); | ||
const PKG = readJSON(`${INSTALL_DIR}/package.json`); | ||
const APP_NAME = PKG.name.split("/")[1]; | ||
const HEADER = ` | ||
\u2588 \u2588 \u2588 \u2502 | ||
\u2588\u2588 \u2588 \u2502 | ||
\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 ${PKG.name} ${PKG.version} | ||
\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 Multi-language data bindings code generator | ||
\u2588 \u2502 | ||
\u2588 \u2588 \u2502 | ||
`; | ||
const usageOpts = { | ||
lineWidth: process.stdout.columns, | ||
prefix: `${HEADER} | ||
lineWidth: process.stdout.columns, | ||
prefix: `${HEADER} | ||
usage: ${APP_NAME} [OPTS] JSON-INPUT-FILE(S) ... | ||
@@ -67,130 +82,146 @@ ${APP_NAME} --help | ||
`, | ||
showGroupNames: true, | ||
paramWidth: 32, | ||
showGroupNames: true, | ||
paramWidth: 32 | ||
}; | ||
const showUsage = () => { | ||
process.stderr.write(usage(argOpts, usageOpts)); | ||
process.exit(1); | ||
process.stderr.write(usage(argOpts, usageOpts)); | ||
process.exit(1); | ||
}; | ||
const invalidSpec = (path, msg) => { | ||
throw new Error(`invalid typedef: ${path}${msg ? ` (${msg})` : ""}`); | ||
throw new Error(`invalid typedef: ${path}${msg ? ` (${msg})` : ""}`); | ||
}; | ||
const addTypeSpec = (ctx, path, coll, spec) => { | ||
if (!(spec.name && spec.type)) | ||
invalidSpec(path); | ||
if (!["enum", "funcptr", "struct", "union"].includes(spec.type)) | ||
invalidSpec(path, `${spec.name} type: ${spec.type}`); | ||
if (coll[spec.name]) | ||
invalidSpec(path, `duplicate name: ${spec.name}`); | ||
if (spec.body) { | ||
if (!isPlainObject(spec.body)) | ||
invalidSpec(path, `${spec.name}.body must be an object`); | ||
for (let lang in spec.body) { | ||
const src = spec.body[lang]; | ||
if (isString(src) && src[0] === "@") { | ||
spec.body[lang] = readText(src.substring(1), ctx.logger); | ||
} | ||
} | ||
if (!(spec.name && spec.type)) | ||
invalidSpec(path); | ||
if (!["enum", "funcptr", "struct", "union"].includes(spec.type)) | ||
invalidSpec(path, `${spec.name} type: ${spec.type}`); | ||
if (coll[spec.name]) | ||
invalidSpec(path, `duplicate name: ${spec.name}`); | ||
if (spec.body) { | ||
if (!isPlainObject(spec.body)) | ||
invalidSpec(path, `${spec.name}.body must be an object`); | ||
for (let lang in spec.body) { | ||
const src = spec.body[lang]; | ||
if (isString(src) && src[0] === "@") { | ||
spec.body[lang] = readText(src.substring(1), ctx.logger); | ||
} | ||
} | ||
ctx.logger.debug(`registering ${spec.type}: ${spec.name}`); | ||
coll[spec.name] = spec; | ||
spec.__path = path; | ||
} | ||
ctx.logger.debug(`registering ${spec.type}: ${spec.name}`); | ||
coll[spec.name] = spec; | ||
spec.__path = path; | ||
}; | ||
const validateTypeRefs = (coll) => { | ||
for (let spec of Object.values(coll)) { | ||
if (!["funcptr", "struct", "union"].includes(spec.type)) | ||
continue; | ||
for (let f of spec.fields || spec.args) { | ||
if (!(isPadding(f) || | ||
isWasmPrim(f.type) || | ||
isSizeT(f.type) || | ||
isOpaque(f.type) || | ||
isWasmString(f.type) || | ||
coll[f.type])) { | ||
invalidSpec(spec.__path, `${spec.name}.${f.name} has unknown type: ${f.type}`); | ||
} | ||
} | ||
for (let spec of Object.values(coll)) { | ||
if (!["funcptr", "struct", "union"].includes(spec.type)) | ||
continue; | ||
for (let f of spec.fields || spec.args) { | ||
if (!(isPadding(f) || isWasmPrim(f.type) || isSizeT(f.type) || isOpaque(f.type) || isWasmString(f.type) || coll[f.type])) { | ||
invalidSpec( | ||
spec.__path, | ||
`${spec.name}.${f.name} has unknown type: ${f.type}` | ||
); | ||
} | ||
} | ||
} | ||
}; | ||
const parseTypeSpecs = (ctx, inputs) => { | ||
const coll = {}; | ||
for (let path of inputs) { | ||
try { | ||
const spec = readJSON(resolve(path), ctx.logger); | ||
if (isArray(spec)) { | ||
for (let s of spec) | ||
addTypeSpec(ctx, path, coll, s); | ||
} | ||
else if (isPlainObject(spec)) { | ||
addTypeSpec(ctx, path, coll, spec); | ||
} | ||
else { | ||
invalidSpec(path); | ||
} | ||
} | ||
catch (e) { | ||
process.stderr.write(e.message); | ||
process.exit(1); | ||
} | ||
const coll = {}; | ||
for (let path of inputs) { | ||
try { | ||
const spec = readJSON(resolve(path), ctx.logger); | ||
if (isArray(spec)) { | ||
for (let s of spec) | ||
addTypeSpec(ctx, path, coll, s); | ||
} else if (isPlainObject(spec)) { | ||
addTypeSpec(ctx, path, coll, spec); | ||
} else { | ||
invalidSpec(path); | ||
} | ||
} catch (e) { | ||
process.stderr.write(e.message); | ||
process.exit(1); | ||
} | ||
validateTypeRefs(coll); | ||
return coll; | ||
} | ||
validateTypeRefs(coll); | ||
return coll; | ||
}; | ||
const generateOutputs = ({ config, logger, opts }, coll) => { | ||
for (let i = 0; i < opts.lang.length; i++) { | ||
const lang = opts.lang[i]; | ||
logger.debug(`generating ${lang.toUpperCase()} output...`); | ||
const src = generateTypes(coll, GENERATORS[lang](config[lang]), config.global); | ||
if (opts.out) { | ||
writeText(resolve(opts.out[i]), src, logger, opts.dryRun); | ||
} | ||
else { | ||
process.stdout.write(src + "\n"); | ||
} | ||
for (let i = 0; i < opts.lang.length; i++) { | ||
const lang = opts.lang[i]; | ||
logger.debug(`generating ${lang.toUpperCase()} output...`); | ||
const src = generateTypes( | ||
coll, | ||
GENERATORS[lang](config[lang]), | ||
config.global | ||
); | ||
if (opts.out) { | ||
writeText(resolve(opts.out[i]), src, logger, opts.dryRun); | ||
} else { | ||
process.stdout.write(src + "\n"); | ||
} | ||
} | ||
}; | ||
const resolveUserCode = (ctx, conf, key) => { | ||
if (conf[key]) { | ||
if (isString(conf[key]) && conf[key][0] === "@") { | ||
conf[key] = readText(resolve(dirname(ctx.opts.config), conf[key].substring(1)), ctx.logger); | ||
} | ||
if (conf[key]) { | ||
if (isString(conf[key]) && conf[key][0] === "@") { | ||
conf[key] = readText( | ||
resolve( | ||
dirname(ctx.opts.config), | ||
conf[key].substring(1) | ||
), | ||
ctx.logger | ||
); | ||
} | ||
} | ||
}; | ||
try { | ||
const result = parse(argOpts, process.argv, { start: 3, usageOpts }); | ||
if (!result) | ||
process.exit(1); | ||
const { result: opts, rest } = result; | ||
if (!rest.length) | ||
showUsage(); | ||
if (opts.out && opts.lang.length != opts.out.length) { | ||
illegalArgs(`expected ${opts.lang.length} outputs, but got ${opts.out.length}`); | ||
const result = parse(argOpts, process.argv, { start: 3, usageOpts }); | ||
if (!result) | ||
process.exit(1); | ||
const { result: opts, rest } = result; | ||
if (!rest.length) | ||
showUsage(); | ||
if (opts.out && opts.lang.length != opts.out.length) { | ||
illegalArgs( | ||
`expected ${opts.lang.length} outputs, but got ${opts.out.length}` | ||
); | ||
} | ||
const ctx = { | ||
logger: new ConsoleLogger("wasm-api", opts.debug ? "DEBUG" : "INFO"), | ||
config: { global: {} }, | ||
opts | ||
}; | ||
if (opts.config) { | ||
opts.config = resolve(opts.config); | ||
ctx.config = readJSON(opts.config, ctx.logger); | ||
for (let id in ctx.config) { | ||
const conf = ctx.config[id]; | ||
resolveUserCode(ctx, conf, "pre"); | ||
resolveUserCode(ctx, conf, "post"); | ||
} | ||
const ctx = { | ||
logger: new ConsoleLogger("wasm-api", opts.debug ? "DEBUG" : "INFO"), | ||
config: { global: {} }, | ||
opts, | ||
}; | ||
if (opts.config) { | ||
opts.config = resolve(opts.config); | ||
ctx.config = readJSON(opts.config, ctx.logger); | ||
for (let id in ctx.config) { | ||
const conf = ctx.config[id]; | ||
resolveUserCode(ctx, conf, "pre"); | ||
resolveUserCode(ctx, conf, "post"); | ||
} | ||
} | ||
opts.debug && mutIn(ctx, ["config", "global", "debug"], true); | ||
opts.string && mutIn(ctx, ["config", "global", "stringType"], opts.string); | ||
const types = parseTypeSpecs(ctx, rest); | ||
generateOutputs(ctx, types); | ||
if (ctx.opts.analytics) { | ||
// always write analytics, even if dry run | ||
writeJSON(resolve(ctx.opts.analytics), types, undefined, "\t", ctx.logger); | ||
} | ||
} | ||
opts.debug && mutIn(ctx, ["config", "global", "debug"], true); | ||
opts.string && mutIn(ctx, ["config", "global", "stringType"], opts.string); | ||
const types = parseTypeSpecs(ctx, rest); | ||
generateOutputs(ctx, types); | ||
if (ctx.opts.analytics) { | ||
writeJSON( | ||
resolve(ctx.opts.analytics), | ||
types, | ||
void 0, | ||
" ", | ||
ctx.logger | ||
); | ||
} | ||
} catch (e) { | ||
if (!(e instanceof ParseError)) | ||
process.stderr.write(e.message); | ||
process.exit(1); | ||
} | ||
catch (e) { | ||
if (!(e instanceof ParseError)) | ||
process.stderr.write(e.message); | ||
process.exit(1); | ||
} | ||
export { | ||
APP_NAME, | ||
HEADER, | ||
INSTALL_DIR, | ||
PKG | ||
}; |
380
codegen.js
@@ -6,218 +6,220 @@ import { SIZEOF } from "@thi.ng/api/typedarray"; | ||
import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; | ||
import { PKG_NAME, WASM32, } from "./api.js"; | ||
import { | ||
PKG_NAME, | ||
WASM32 | ||
} from "./api.js"; | ||
import { selectAlignment } from "./internal/align.js"; | ||
import { ensureStringArray, isBigNumeric, isNumeric, isOpaque, isPointer, isPointerLike, isSizeT, isSlice, isStringSlice, isWasmString, } from "./internal/utils.js"; | ||
export const DEFAULT_CODEGEN_OPTS = { | ||
debug: false, | ||
header: true, | ||
lineWidth: 80, | ||
stringType: "ptr", | ||
target: WASM32, | ||
uppercaseEnums: true, | ||
import { | ||
ensureStringArray, | ||
isBigNumeric, | ||
isNumeric, | ||
isOpaque, | ||
isPointer, | ||
isPointerLike, | ||
isSizeT, | ||
isSlice, | ||
isStringSlice, | ||
isWasmString | ||
} from "./internal/utils.js"; | ||
const DEFAULT_CODEGEN_OPTS = { | ||
debug: false, | ||
header: true, | ||
lineWidth: 80, | ||
stringType: "ptr", | ||
target: WASM32, | ||
uppercaseEnums: true | ||
}; | ||
const sizeOf = defmulti((x) => x.type, {}, { | ||
const sizeOf = defmulti( | ||
(x) => x.type, | ||
{}, | ||
{ | ||
[DEFAULT]: (field, coll, align, opts) => { | ||
if (field.__size) | ||
return field.__size; | ||
if (field.pad != null) { | ||
field.pad < 1 && illegalArgs(`pad size must be > 0`); | ||
return (field.__size = field.pad); | ||
if (field.__size) | ||
return field.__size; | ||
if (field.pad != null) { | ||
field.pad < 1 && illegalArgs(`pad size must be > 0`); | ||
return field.__size = field.pad; | ||
} | ||
let size = 0; | ||
if (isPointer(field.tag)) { | ||
size = opts.target.sizeBytes; | ||
} else if (isSlice(field.tag)) { | ||
size = opts.target.sizeBytes * 2; | ||
} else { | ||
size = isNumeric(field.type) || isBigNumeric(field.type) ? SIZEOF[field.type] : isWasmString(field.type) ? opts.target.sizeBytes * (isStringSlice(opts.stringType) ? 2 : 1) : isOpaque(field.type) ? opts.target.sizeBytes : sizeOf(coll[field.type], coll, align, opts); | ||
if (field.tag == "array" || field.tag === "vec") { | ||
size *= field.len; | ||
if (field.sentinel !== void 0 && field.tag === "array") { | ||
size += SIZEOF[field.type]; | ||
} | ||
} | ||
let size = 0; | ||
if (isPointer(field.tag)) { | ||
size = opts.target.sizeBytes; | ||
} | ||
else if (isSlice(field.tag)) { | ||
size = opts.target.sizeBytes * 2; | ||
} | ||
else { | ||
size = | ||
isNumeric(field.type) || isBigNumeric(field.type) | ||
? SIZEOF[field.type] | ||
: isWasmString(field.type) | ||
? opts.target.sizeBytes * | ||
(isStringSlice(opts.stringType) ? 2 : 1) | ||
: isOpaque(field.type) | ||
? opts.target.sizeBytes | ||
: sizeOf(coll[field.type], coll, align, opts); | ||
if (field.tag == "array" || field.tag === "vec") { | ||
size *= field.len; | ||
if (field.sentinel !== undefined && field.tag === "array") { | ||
size += SIZEOF[field.type]; | ||
} | ||
} | ||
} | ||
return (field.__size = align.size(size, field.__align)); | ||
} | ||
return field.__size = align.size(size, field.__align); | ||
}, | ||
enum: (type) => { | ||
if (type.__size) | ||
return type.__size; | ||
return (type.__size = SIZEOF[type.tag]); | ||
if (type.__size) | ||
return type.__size; | ||
return type.__size = SIZEOF[type.tag]; | ||
}, | ||
struct: (type, coll, align, opts) => { | ||
if (type.__size) | ||
return type.__size; | ||
let offset = 0; | ||
for (let f of type.fields) { | ||
offset = align.offset(offset, f.__align); | ||
f.__offset = offset; | ||
offset += sizeOf(f, coll, align, opts); | ||
} | ||
return (type.__size = align.size(offset, type.__align)); | ||
if (type.__size) | ||
return type.__size; | ||
let offset = 0; | ||
for (let f of type.fields) { | ||
offset = align.offset(offset, f.__align); | ||
f.__offset = offset; | ||
offset += sizeOf(f, coll, align, opts); | ||
} | ||
return type.__size = align.size(offset, type.__align); | ||
}, | ||
union: (type, coll, align, opts) => { | ||
if (type.__size) | ||
return type.__size; | ||
let maxSize = 0; | ||
for (let f of type.fields) { | ||
f.__offset = 0; | ||
maxSize = Math.max(maxSize, sizeOf(f, coll, align, opts)); | ||
} | ||
return (type.__size = align.size(maxSize, type.__align)); | ||
if (type.__size) | ||
return type.__size; | ||
let maxSize = 0; | ||
for (let f of type.fields) { | ||
f.__offset = 0; | ||
maxSize = Math.max(maxSize, sizeOf(f, coll, align, opts)); | ||
} | ||
return type.__size = align.size(maxSize, type.__align); | ||
}, | ||
funcptr: (type, _, __, opts) => { | ||
return type.__size || (type.__size = opts.target.sizeBytes); | ||
}, | ||
}); | ||
const alignOf = defmulti((x) => x.type, {}, { | ||
return type.__size || (type.__size = opts.target.sizeBytes); | ||
} | ||
} | ||
); | ||
const alignOf = defmulti( | ||
(x) => x.type, | ||
{}, | ||
{ | ||
[DEFAULT]: (field, coll, align, opts) => { | ||
if (field.__align) | ||
return field.__align; | ||
if (field.pad) | ||
return (field.__align = 1); | ||
if (isSizeT(field.type)) { | ||
field.type = opts.target[field.type]; | ||
} | ||
return (field.__align = isPointerLike(field, coll) | ||
? align.align({ type: opts.target.usize }) | ||
: isNumeric(field.type) || isBigNumeric(field.type) | ||
? align.align(field) | ||
: alignOf(coll[field.type], coll, selectAlignment(coll[field.type]), opts)); | ||
if (field.__align) | ||
return field.__align; | ||
if (field.pad) | ||
return field.__align = 1; | ||
if (isSizeT(field.type)) { | ||
field.type = opts.target[field.type]; | ||
} | ||
return field.__align = isPointerLike(field, coll) ? align.align({ type: opts.target.usize }) : isNumeric(field.type) || isBigNumeric(field.type) ? align.align(field) : alignOf( | ||
coll[field.type], | ||
coll, | ||
selectAlignment(coll[field.type]), | ||
opts | ||
); | ||
}, | ||
enum: (type, _, align) => { | ||
const e = type; | ||
if (!e.tag) | ||
e.tag = "i32"; | ||
return (e.__align = align.align({ | ||
type: e.tag, | ||
})); | ||
const e = type; | ||
if (!e.tag) | ||
e.tag = "i32"; | ||
return e.__align = align.align({ | ||
type: e.tag | ||
}); | ||
}, | ||
struct: (type, coll, align, opts) => { | ||
let maxAlign = 1; | ||
for (let f of type.fields) { | ||
maxAlign = Math.max(maxAlign, alignOf(f, coll, align, opts)); | ||
} | ||
return (type.__align = maxAlign); | ||
let maxAlign = 1; | ||
for (let f of type.fields) { | ||
maxAlign = Math.max(maxAlign, alignOf(f, coll, align, opts)); | ||
} | ||
return type.__align = maxAlign; | ||
}, | ||
union: (type, coll, align, opts) => { | ||
let maxAlign = 1; | ||
for (let f of type.fields) { | ||
maxAlign = Math.max(maxAlign, alignOf(f, coll, align, opts)); | ||
} | ||
return (type.__align = maxAlign); | ||
let maxAlign = 1; | ||
for (let f of type.fields) { | ||
maxAlign = Math.max(maxAlign, alignOf(f, coll, align, opts)); | ||
} | ||
return type.__align = maxAlign; | ||
}, | ||
funcptr: (type, coll, align, opts) => { | ||
if (type.__align) | ||
return type.__align; | ||
const ptr = type; | ||
if (ptr.rtype !== "void") { | ||
sizeOf(ptr.rtype, coll, align, opts); | ||
} | ||
for (let a of ptr.args) { | ||
alignOf(a, coll, align, opts); | ||
} | ||
return (type.__align = align.align({ | ||
type: opts.target.usize, | ||
})); | ||
}, | ||
}); | ||
const prepareType = defmulti((x) => x.type, {}, { | ||
if (type.__align) | ||
return type.__align; | ||
const ptr = type; | ||
if (ptr.rtype !== "void") { | ||
sizeOf(ptr.rtype, coll, align, opts); | ||
} | ||
for (let a of ptr.args) { | ||
alignOf(a, coll, align, opts); | ||
} | ||
return type.__align = align.align({ | ||
type: opts.target.usize | ||
}); | ||
} | ||
} | ||
); | ||
const prepareType = defmulti( | ||
(x) => x.type, | ||
{}, | ||
{ | ||
[DEFAULT]: (x, coll, alignImpl, opts) => { | ||
if (x.__align) | ||
return; | ||
alignOf(x, coll, alignImpl, opts); | ||
sizeOf(x, coll, alignImpl, opts); | ||
if (x.__align) | ||
return; | ||
alignOf(x, coll, alignImpl, opts); | ||
sizeOf(x, coll, alignImpl, opts); | ||
}, | ||
struct: (x, coll, align, opts) => { | ||
if (x.__align) | ||
return; | ||
const struct = x; | ||
alignOf(struct, coll, align, opts); | ||
if (struct.auto) { | ||
struct.fields.sort(compareByKey("__align", compareNumDesc)); | ||
if (x.__align) | ||
return; | ||
const struct = x; | ||
alignOf(struct, coll, align, opts); | ||
if (struct.auto) { | ||
struct.fields.sort( | ||
compareByKey("__align", compareNumDesc) | ||
); | ||
} | ||
for (let f of struct.fields) { | ||
const type = coll[f.type]; | ||
if (type) { | ||
prepareType(type, coll, selectAlignment(type), opts); | ||
} | ||
for (let f of struct.fields) { | ||
const type = coll[f.type]; | ||
if (type) { | ||
prepareType(type, coll, selectAlignment(type), opts); | ||
} | ||
} | ||
sizeOf(struct, coll, align, opts); | ||
}, | ||
}); | ||
/** | ||
* Takes a type collection and analyzes each analyzed to compute individual | ||
* alignments and sizes. | ||
* | ||
* @remarks | ||
* This function is idempotent and called automatically by | ||
* {@link generateTypes}. Only exported for dev/debug purposes. | ||
* | ||
* @param coll | ||
* | ||
* @internal | ||
*/ | ||
export const prepareTypes = (coll, opts) => { | ||
for (let id in coll) { | ||
prepareType(coll[id], coll, selectAlignment(coll[id]), opts); | ||
} | ||
sizeOf(struct, coll, align, opts); | ||
} | ||
return coll; | ||
} | ||
); | ||
const prepareTypes = (coll, opts) => { | ||
for (let id in coll) { | ||
prepareType(coll[id], coll, selectAlignment(coll[id]), opts); | ||
} | ||
return coll; | ||
}; | ||
/** | ||
* Code generator main entry point. Takes an object of {@link TopLevelType} | ||
* definitions, an actual code generator implementation for a single target | ||
* language and (optional) global codegen options. Returns generated source code | ||
* for all given types as a single string. | ||
* | ||
* @remarks | ||
* Before actual code generation the types are first analyzed to compute their | ||
* alignments and sizes. This is only ever done once (idempotent), even if | ||
* `generateTypes()` is called multiple times for different target langs. | ||
* | ||
* @param coll | ||
* @param codegen | ||
* @param opts | ||
*/ | ||
export const generateTypes = (coll, codegen, opts = {}) => { | ||
const $opts = { | ||
...DEFAULT_CODEGEN_OPTS, | ||
...opts, | ||
}; | ||
prepareTypes(coll, $opts); | ||
const res = []; | ||
if ($opts.header) { | ||
codegen.doc([ | ||
`Generated by ${PKG_NAME} at ${new Date().toISOString()}`, | ||
"DO NOT EDIT!", | ||
], res, $opts, true); | ||
res.push(""); | ||
} | ||
if (codegen.pre) { | ||
const pre = codegen.pre(coll, $opts); | ||
pre && res.push(pre, ""); | ||
} | ||
$opts.pre && res.push(...ensureStringArray($opts.pre), ""); | ||
for (let id in coll) { | ||
const type = coll[id]; | ||
if (type.skip?.includes(codegen.id)) | ||
continue; | ||
type.doc && codegen.doc(type.doc, res, $opts); | ||
codegen[type.type](type, coll, res, $opts); | ||
} | ||
$opts.post && res.push("", ...ensureStringArray($opts.post)); | ||
if (codegen.post) { | ||
const post = codegen.post(coll, $opts); | ||
post && res.push("", post); | ||
} | ||
return res.join("\n"); | ||
const generateTypes = (coll, codegen, opts = {}) => { | ||
const $opts = { | ||
...DEFAULT_CODEGEN_OPTS, | ||
...opts | ||
}; | ||
prepareTypes(coll, $opts); | ||
const res = []; | ||
if ($opts.header) { | ||
codegen.doc( | ||
[ | ||
`Generated by ${PKG_NAME} at ${(/* @__PURE__ */ new Date()).toISOString()}`, | ||
"DO NOT EDIT!" | ||
], | ||
res, | ||
$opts, | ||
true | ||
); | ||
res.push(""); | ||
} | ||
if (codegen.pre) { | ||
const pre = codegen.pre(coll, $opts); | ||
pre && res.push(pre, ""); | ||
} | ||
$opts.pre && res.push(...ensureStringArray($opts.pre), ""); | ||
for (let id in coll) { | ||
const type = coll[id]; | ||
if (type.skip?.includes(codegen.id)) | ||
continue; | ||
type.doc && codegen.doc(type.doc, res, $opts); | ||
codegen[type.type](type, coll, res, $opts); | ||
} | ||
$opts.post && res.push("", ...ensureStringArray($opts.post)); | ||
if (codegen.post) { | ||
const post = codegen.post(coll, $opts); | ||
post && res.push("", post); | ||
} | ||
return res.join("\n"); | ||
}; | ||
export { | ||
DEFAULT_CODEGEN_OPTS, | ||
generateTypes, | ||
prepareTypes | ||
}; |
@@ -5,27 +5,23 @@ import { SIZEOF } from "@thi.ng/api/typedarray"; | ||
import { isStruct, isUnion } from "./utils.js"; | ||
/** | ||
* C ABI compatible alignment | ||
*/ | ||
export const ALIGN_C = { | ||
align: (field) => { | ||
let align = SIZEOF[field.type]; | ||
if (field.tag === "vec") { | ||
align *= ceilPow2(field.len); | ||
} | ||
return align; | ||
}, | ||
size: (size, align) => $align(size, align), | ||
offset: (offset, align) => $align(offset, align), | ||
const ALIGN_C = { | ||
align: (field) => { | ||
let align = SIZEOF[field.type]; | ||
if (field.tag === "vec") { | ||
align *= ceilPow2(field.len); | ||
} | ||
return align; | ||
}, | ||
size: (size, align) => $align(size, align), | ||
offset: (offset, align) => $align(offset, align) | ||
}; | ||
export const ALIGN_PACKED = { | ||
align: () => 1, | ||
size: (size) => size, | ||
offset: (offset) => offset, | ||
const ALIGN_PACKED = { | ||
align: () => 1, | ||
size: (size) => size, | ||
offset: (offset) => offset | ||
}; | ||
/** | ||
* Returns a suitable alignment strategy for given type, i.e. either the user | ||
* supplied impl defined for the type or the default ({@link ALIGN_C}). | ||
* | ||
* @param type | ||
*/ | ||
export const selectAlignment = (type) => isStruct(type) || isUnion(type) ? type.align || ALIGN_C : ALIGN_C; | ||
const selectAlignment = (type) => isStruct(type) || isUnion(type) ? type.align || ALIGN_C : ALIGN_C; | ||
export { | ||
ALIGN_C, | ||
ALIGN_PACKED, | ||
selectAlignment | ||
}; |
import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; | ||
import { isEnum, isOpaque, isPadding, isPointer, isSlice, isWasmPrim, isWasmString, } from "./utils.js"; | ||
/** | ||
* Analyses and classifies given field and returns a classifier constant used by | ||
* each codegen to emit the relevant syntax. | ||
* | ||
* @param field | ||
* @param coll | ||
* | ||
* @internal | ||
*/ | ||
export const classifyField = (field, coll) => { | ||
if (isPadding(field)) | ||
return { classifier: "pad", isConst: false }; | ||
const $isPrim = isWasmPrim(field.type); | ||
const $isStr = isWasmString(field.type); | ||
const $isSlice = isSlice(field.tag); | ||
const $isPtr = isPointer(field.tag); | ||
const $isEnum = isEnum(field.type, coll); | ||
const $isArray = field.tag === "array"; | ||
const $isVec = field.tag === "vec"; | ||
if ($isVec && !$isPrim) { | ||
illegalArgs("only vectors of numeric types allowed"); | ||
import { | ||
isEnum, | ||
isOpaque, | ||
isPadding, | ||
isPointer, | ||
isSlice, | ||
isWasmPrim, | ||
isWasmString | ||
} from "./utils.js"; | ||
const classifyField = (field, coll) => { | ||
if (isPadding(field)) | ||
return { classifier: "pad", isConst: false }; | ||
const $isPrim = isWasmPrim(field.type); | ||
const $isStr = isWasmString(field.type); | ||
const $isSlice = isSlice(field.tag); | ||
const $isPtr = isPointer(field.tag); | ||
const $isEnum = isEnum(field.type, coll); | ||
const $isArray = field.tag === "array"; | ||
const $isVec = field.tag === "vec"; | ||
if ($isVec && !$isPrim) { | ||
illegalArgs("only vectors of numeric types allowed"); | ||
} | ||
if (field.sentinel != null && !$isPrim) { | ||
illegalArgs("sentinels only supported for numeric types"); | ||
} | ||
let id = "single"; | ||
let $const = !!field.const; | ||
if ($isStr) { | ||
$const = field.const !== false; | ||
id = __classifyType("str", $isArray, $isSlice, $isPtr, field.len); | ||
} else if (isOpaque(field.type)) { | ||
id = __classifyType("opaque", $isArray, $isSlice, $isPtr, field.len); | ||
} else if ($isEnum) { | ||
id = __classifyType("enum", $isArray, $isSlice, $isPtr, field.len); | ||
} else { | ||
if ($isArray || $isVec) { | ||
__ensureLength(field.len); | ||
id = $isArray ? "array" : "vec"; | ||
} else if ($isSlice) { | ||
id = "slice"; | ||
} else if ($isPtr) { | ||
id = __selectForLength(field.len, "ptr", "ptrFixed", "ptrMulti"); | ||
} | ||
if (field.sentinel != null && !$isPrim) { | ||
illegalArgs("sentinels only supported for numeric types"); | ||
} | ||
let id = "single"; | ||
let $const = !!field.const; | ||
if ($isStr) { | ||
$const = field.const !== false; | ||
id = __classifyType("str", $isArray, $isSlice, $isPtr, field.len); | ||
} | ||
else if (isOpaque(field.type)) { | ||
id = __classifyType("opaque", $isArray, $isSlice, $isPtr, field.len); | ||
} | ||
else if ($isEnum) { | ||
id = __classifyType("enum", $isArray, $isSlice, $isPtr, field.len); | ||
} | ||
else { | ||
if ($isArray || $isVec) { | ||
__ensureLength(field.len); | ||
id = $isArray ? "array" : "vec"; | ||
} | ||
else if ($isSlice) { | ||
id = "slice"; | ||
} | ||
else if ($isPtr) { | ||
id = __selectForLength(field.len, "ptr", "ptrFixed", "ptrMulti"); | ||
} | ||
} | ||
return { classifier: id, isConst: $const }; | ||
} | ||
return { classifier: id, isConst: $const }; | ||
}; | ||
const __classifyType = (base, $isArray, $isSlice, $isPtr, len) => { | ||
if ($isArray) { | ||
__ensureLength(len); | ||
return (base + "Array"); | ||
} | ||
else if ($isSlice) { | ||
return (base + "Slice"); | ||
} | ||
else if ($isPtr) { | ||
return __selectForLength(len, (base + "Ptr"), (base + "PtrFixed"), (base + "PtrMulti")); | ||
} | ||
return base; | ||
if ($isArray) { | ||
__ensureLength(len); | ||
return base + "Array"; | ||
} else if ($isSlice) { | ||
return base + "Slice"; | ||
} else if ($isPtr) { | ||
return __selectForLength( | ||
len, | ||
base + "Ptr", | ||
base + "PtrFixed", | ||
base + "PtrMulti" | ||
); | ||
} | ||
return base; | ||
}; | ||
/** @internal */ | ||
const __selectForLength = (len, single, fixed, multi) => (len != null ? (len > 0 ? fixed : multi) : single); | ||
/** @internal */ | ||
const __selectForLength = (len, single, fixed, multi) => len != null ? len > 0 ? fixed : multi : single; | ||
const __ensureLength = (len) => { | ||
if (len == null || len < 1) | ||
illegalArgs("missing or invalid array/vec length"); | ||
if (len == null || len < 1) | ||
illegalArgs("missing or invalid array/vec length"); | ||
}; | ||
export { | ||
classifyField | ||
}; |
@@ -6,132 +6,61 @@ import { isArray } from "@thi.ng/checks/is-array"; | ||
import { wordWrapLine, wordWrapLines } from "@thi.ng/strings/word-wrap"; | ||
/** | ||
* Returns true iff `x` is a {@link WasmPrim32}. | ||
* | ||
* @param x | ||
*/ | ||
export const isNumeric = (x) => /^(([iu](8|16|32))|(f(32|64)))$/.test(x); | ||
/** | ||
* Returns true iff `x` is a `i64` or `u64`. | ||
* | ||
* @param x | ||
*/ | ||
export const isBigNumeric = (x) => /^[iu]64$/.test(x); | ||
export const isSizeT = (x) => /^[iu]size$/.test(x); | ||
/** | ||
* Returns true iff `x` is a {@link WasmPrim}. | ||
* | ||
* @param x | ||
*/ | ||
export const isWasmPrim = (x) => isNumeric(x) || isBigNumeric(x); | ||
export const isWasmString = (x) => x === "string"; | ||
export const isPadding = (f) => f.pad != null && f.pad > 0; | ||
export const isPointer = (x) => x === "ptr"; | ||
export const isFuncPointer = (type, coll) => coll[type]?.type === "funcptr"; | ||
export const isEnum = (type, coll) => coll[type]?.type === "enum"; | ||
export const isSlice = (x) => x === "slice"; | ||
export const isOpaque = (x) => x === "opaque"; | ||
/** | ||
* Returns true iff the struct field is a pointer, slice or "string" type | ||
* | ||
* @param f | ||
*/ | ||
export const isPointerLike = (f, coll) => isPointer(f.tag) || | ||
isSlice(f.tag) || | ||
isWasmString(f.type) || | ||
isOpaque(f.type) || | ||
isFuncPointer(f.type, coll); | ||
/** | ||
* Returns true if `type` is "slice". | ||
* | ||
* @param type | ||
*/ | ||
export const isStringSlice = (type) => type === "slice"; | ||
export const isStruct = (x) => x.type === "struct"; | ||
export const isUnion = (x) => x.type === "union"; | ||
/** | ||
* Returns filtered array of struct fields of with "ptr" tag or function | ||
* pointers. | ||
* | ||
* @param fields | ||
* | ||
* @internal | ||
*/ | ||
export const pointerFields = (fields) => fields.filter((f) => isPointer(f.tag)); | ||
/** | ||
* Returns filtered array of struct fields of only "string" fields. | ||
* | ||
* @param fields | ||
* | ||
* @internal | ||
*/ | ||
export const stringFields = (fields) => fields.filter((f) => isWasmString(f.type) && f.tag !== "ptr"); | ||
export const sliceTypes = (coll) => new Set(Object.values(coll) | ||
.flatMap((x) => (isStruct(x) || isUnion(x) ? x.fields : [])) | ||
.map((x) => (x.tag === "slice" ? x.type : null)) | ||
.filter((x) => !!x)); | ||
/** | ||
* Returns enum identifier formatted according to given opts. | ||
* | ||
* @param opts | ||
* @param name | ||
* | ||
* @internal | ||
*/ | ||
export const enumName = (opts, name) => opts.uppercaseEnums ? name.toUpperCase() : name; | ||
/** | ||
* Returns given field's default value (or undefined). The `lang` ID is required | ||
* to obtain the language specific value if the default is given as object. | ||
* | ||
* @param f | ||
* @param lang | ||
*/ | ||
export const defaultValue = (f, lang) => f.default !== undefined | ||
? isPlainObject(f.default) | ||
? f.default[lang] | ||
: f.default | ||
: undefined; | ||
/** | ||
* Takes an array of strings or splits given string into lines, word wraps and | ||
* then prefixes each line with given `width` and `prefix`. Returns array of new | ||
* lines. | ||
* | ||
* @param prefix | ||
* @param str | ||
* @param width | ||
*/ | ||
export const prefixLines = (prefix, str, width) => (isString(str) | ||
? wordWrapLines(str, { width: width - prefix.length }) | ||
: str.flatMap((x) => wordWrapLine(x, { width: width - prefix.length }))).map((line) => prefix + line); | ||
export const ensureLines = (src, key) => isString(src) | ||
? split(src) | ||
: isArray(src) | ||
? src | ||
: key | ||
? src[key] | ||
? ensureLines(src[key], key) | ||
: [] | ||
: []; | ||
export const ensureStringArray = (src) => isString(src) ? [src] : src; | ||
/** | ||
* Yields iterator of given lines, each with applied indentation based on given | ||
* scope regexp's which are applied to each line to increase or decrease | ||
* indentation level (the initial indentation level can be specified via | ||
* optional `level` arg, default 0). If `scopeStart` succeeds, the indent is | ||
* increased for the _next_ line. If `scopeEnd` succeeds the level is decreased | ||
* for the _current_ line. ... | ||
* | ||
* @param lines | ||
* @param indent | ||
* @param scopeStart | ||
* @param scopeEnd | ||
* @param level | ||
*/ | ||
export function* withIndentation(lines, indent, scopeStart, scopeEnd, level = 0) { | ||
const stack = new Array(level).fill(indent); | ||
for (let l of lines) { | ||
scopeEnd.test(l) && stack.pop(); | ||
const curr = stack.length ? stack[stack.length - 1] : ""; | ||
yield curr + l; | ||
scopeStart.test(l) && stack.push(curr + indent); | ||
} | ||
const isNumeric = (x) => /^(([iu](8|16|32))|(f(32|64)))$/.test(x); | ||
const isBigNumeric = (x) => /^[iu]64$/.test(x); | ||
const isSizeT = (x) => /^[iu]size$/.test(x); | ||
const isWasmPrim = (x) => isNumeric(x) || isBigNumeric(x); | ||
const isWasmString = (x) => x === "string"; | ||
const isPadding = (f) => f.pad != null && f.pad > 0; | ||
const isPointer = (x) => x === "ptr"; | ||
const isFuncPointer = (type, coll) => coll[type]?.type === "funcptr"; | ||
const isEnum = (type, coll) => coll[type]?.type === "enum"; | ||
const isSlice = (x) => x === "slice"; | ||
const isOpaque = (x) => x === "opaque"; | ||
const isPointerLike = (f, coll) => isPointer(f.tag) || isSlice(f.tag) || isWasmString(f.type) || isOpaque(f.type) || isFuncPointer(f.type, coll); | ||
const isStringSlice = (type) => type === "slice"; | ||
const isStruct = (x) => x.type === "struct"; | ||
const isUnion = (x) => x.type === "union"; | ||
const pointerFields = (fields) => fields.filter((f) => isPointer(f.tag)); | ||
const stringFields = (fields) => fields.filter((f) => isWasmString(f.type) && f.tag !== "ptr"); | ||
const sliceTypes = (coll) => new Set( | ||
Object.values(coll).flatMap((x) => isStruct(x) || isUnion(x) ? x.fields : []).map((x) => x.tag === "slice" ? x.type : null).filter((x) => !!x) | ||
); | ||
const enumName = (opts, name) => opts.uppercaseEnums ? name.toUpperCase() : name; | ||
const defaultValue = (f, lang) => f.default !== void 0 ? isPlainObject(f.default) ? f.default[lang] : f.default : void 0; | ||
const prefixLines = (prefix, str, width) => (isString(str) ? wordWrapLines(str, { width: width - prefix.length }) : str.flatMap((x) => wordWrapLine(x, { width: width - prefix.length }))).map((line) => prefix + line); | ||
const ensureLines = (src, key) => isString(src) ? split(src) : isArray(src) ? src : key ? src[key] ? ensureLines(src[key], key) : [] : []; | ||
const ensureStringArray = (src) => isString(src) ? [src] : src; | ||
function* withIndentation(lines, indent, scopeStart, scopeEnd, level = 0) { | ||
const stack = new Array(level).fill(indent); | ||
for (let l of lines) { | ||
scopeEnd.test(l) && stack.pop(); | ||
const curr = stack.length ? stack[stack.length - 1] : ""; | ||
yield curr + l; | ||
scopeStart.test(l) && stack.push(curr + indent); | ||
} | ||
} | ||
export { | ||
defaultValue, | ||
ensureLines, | ||
ensureStringArray, | ||
enumName, | ||
isBigNumeric, | ||
isEnum, | ||
isFuncPointer, | ||
isNumeric, | ||
isOpaque, | ||
isPadding, | ||
isPointer, | ||
isPointerLike, | ||
isSizeT, | ||
isSlice, | ||
isStringSlice, | ||
isStruct, | ||
isUnion, | ||
isWasmPrim, | ||
isWasmString, | ||
pointerFields, | ||
prefixLines, | ||
sliceTypes, | ||
stringFields, | ||
withIndentation | ||
}; |
{ | ||
"name": "@thi.ng/wasm-api-bindgen", | ||
"version": "0.4.32", | ||
"version": "0.4.33", | ||
"description": "Polyglot bindings code generators for hybrid JS & WebAssembly projects", | ||
@@ -28,3 +28,5 @@ "type": "module", | ||
"scripts": { | ||
"build": "yarn clean && tsc --declaration", | ||
"build": "yarn build:esbuild && yarn build:decl", | ||
"build:decl": "tsc --declaration --emitDeclarationOnly", | ||
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts", | ||
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc internal", | ||
@@ -39,18 +41,19 @@ "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts", | ||
"dependencies": { | ||
"@thi.ng/api": "^8.9.11", | ||
"@thi.ng/args": "^2.2.45", | ||
"@thi.ng/arrays": "^2.7.7", | ||
"@thi.ng/binary": "^3.3.40", | ||
"@thi.ng/checks": "^3.4.11", | ||
"@thi.ng/compare": "^2.2.7", | ||
"@thi.ng/defmulti": "^3.0.9", | ||
"@thi.ng/errors": "^2.4.5", | ||
"@thi.ng/file-io": "^1.0.4", | ||
"@thi.ng/logger": "^2.0.1", | ||
"@thi.ng/paths": "^5.1.52", | ||
"@thi.ng/strings": "^3.7.2", | ||
"@thi.ng/wasm-api": "^1.4.36" | ||
"@thi.ng/api": "^8.9.12", | ||
"@thi.ng/args": "^2.2.46", | ||
"@thi.ng/arrays": "^2.7.8", | ||
"@thi.ng/binary": "^3.4.0", | ||
"@thi.ng/checks": "^3.4.12", | ||
"@thi.ng/compare": "^2.2.8", | ||
"@thi.ng/defmulti": "^3.0.10", | ||
"@thi.ng/errors": "^2.4.6", | ||
"@thi.ng/file-io": "^1.0.5", | ||
"@thi.ng/logger": "^2.0.2", | ||
"@thi.ng/paths": "^5.1.53", | ||
"@thi.ng/strings": "^3.7.3", | ||
"@thi.ng/wasm-api": "^1.4.37" | ||
}, | ||
"devDependencies": { | ||
"@microsoft/api-extractor": "^7.38.3", | ||
"esbuild": "^0.19.8", | ||
"rimraf": "^5.0.5", | ||
@@ -123,3 +126,3 @@ "tools": "^0.0.1", | ||
}, | ||
"gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n" | ||
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n" | ||
} |
@@ -1,352 +0,391 @@ | ||
import { BIGINT_ARRAY_CTORS, BIT_SHIFTS, TYPEDARRAY_CTORS, } from "@thi.ng/api/typedarray"; | ||
import { | ||
BIGINT_ARRAY_CTORS, | ||
BIT_SHIFTS, | ||
TYPEDARRAY_CTORS | ||
} from "@thi.ng/api/typedarray"; | ||
import { isString } from "@thi.ng/checks/is-string"; | ||
import { unsupported } from "@thi.ng/errors/unsupported"; | ||
import { classifyField } from "./internal/classify.js"; | ||
import { ensureLines, ensureStringArray, enumName, isEnum, isFuncPointer, isNumeric, isOpaque, isPointer, isStringSlice, isWasmPrim, isWasmString, prefixLines, withIndentation, } from "./internal/utils.js"; | ||
import { | ||
ensureLines, | ||
ensureStringArray, | ||
enumName, | ||
isEnum, | ||
isFuncPointer, | ||
isNumeric, | ||
isOpaque, | ||
isPointer, | ||
isStringSlice, | ||
isWasmPrim, | ||
isWasmString, | ||
prefixLines, | ||
withIndentation | ||
} from "./internal/utils.js"; | ||
import { fieldType as zigFieldType } from "./zig.js"; | ||
/** | ||
* TypeScript code generator. Call with options and then pass to | ||
* {@link generateTypes} (see its docs for further usage). | ||
* | ||
* @remarks | ||
* This codegen generates interface and enum definitions for a {@link TypeColl} | ||
* given to {@link generateTypes}. For structs it will also generate memory | ||
* mapped wrappers with fully typed accessors. | ||
* | ||
* @param opts | ||
*/ | ||
export const TYPESCRIPT = (opts = {}) => { | ||
const { indent } = { | ||
indent: "\t", | ||
stringType: "slice", | ||
...opts, | ||
}; | ||
const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/]; | ||
const gen = { | ||
id: "ts", | ||
pre: (_, globalOpts) => { | ||
const res = [ | ||
"// @ts-ignore possibly includes unused imports", | ||
`import { Pointer, ${__stringImpl(globalOpts)}, type MemorySlice, type WasmTypeBase, type WasmTypeConstructor } from "@thi.ng/wasm-api";`, | ||
]; | ||
if (opts.pre) | ||
res.push("", ...ensureStringArray(opts.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => opts.post | ||
? isString(opts.post) | ||
? opts.post | ||
: opts.post.join("\n") | ||
: "", | ||
doc: (doc, acc, opts) => { | ||
acc.push("/**", ...prefixLines(" * ", doc, opts.lineWidth), " */"); | ||
}, | ||
enum: (e, _, acc, opts) => { | ||
const res = []; | ||
res.push(`export enum ${e.name} {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, res, opts); | ||
line = enumName(opts, v.name); | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} | ||
else { | ||
line = enumName(opts, v); | ||
} | ||
res.push(line + ","); | ||
} | ||
res.push("}", ""); | ||
acc.push(...withIndentation(res, indent, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts) => { | ||
const strType = __stringImpl(opts); | ||
const fields = (struct.fields | ||
.map((f) => generateField(f, coll, opts)) | ||
.filter((f) => !!f)); | ||
const lines = []; | ||
lines.push(`export interface ${struct.name} extends WasmTypeBase {`); | ||
for (let f of fields) { | ||
const doc = __docType(f.field, struct, coll, opts); | ||
doc && gen.doc(doc, lines, opts); | ||
lines.push(`${f.field.name}: ${f.type};`); | ||
} | ||
if (struct.body?.ts) { | ||
lines.push("", ...ensureLines(struct.body.ts, "decl")); | ||
} | ||
lines.push("}", ""); | ||
const pointerDecls = fields | ||
.filter((f) => isPointer(f.field.tag) && f.field.len !== 0) | ||
.map((f) => { | ||
return `let $${f.field.name}: ${f.type} | null = null;`; | ||
}); | ||
const stringDecls = fields | ||
.filter((f) => isWasmString(f.field.type) && | ||
!["array", "ptr", "slice"].includes(f.field.tag)) | ||
.map((f) => `let $${f.field.name}: ${strType} | null = null;`); | ||
// type implementation | ||
lines.push(`export const $${struct.name}: WasmTypeConstructor<${struct.name}> = (mem) => ({`, `get align() {`, `return ${struct.__align};`, `},`, `get size() {`, `return ${struct.__size};`, `},`, `instanceArray(base, num) {`, `const items: ${struct.name}[] = [];`, `for (; num --> 0; base += ${struct.__size}) items.push(this.instance(base));`, `return items;`, `},`, `instance: (base) => {`, ...pointerDecls, ...stringDecls, `return {`, `get __base() {`, `return base;`, `},`, `get __bytes() {`, `return mem.u8.subarray(base, base + ${struct.__size});`, `},`); | ||
for (let f of fields) { | ||
if (!f) | ||
continue; | ||
lines.push(`get ${f.field.name}(): ${f.type} {`, ...f.getter, "},"); | ||
if (f.setter) { | ||
lines.push(`set ${f.field.name}(x: ${f.type}) {`, ...f.setter, "},"); | ||
} | ||
} | ||
if (struct.body?.ts) { | ||
lines.push("", ...ensureLines(struct.body.ts, "impl"), ""); | ||
} | ||
lines.push("};", "}", "});", ""); | ||
acc.push(...withIndentation(lines, indent, ...SCOPES)); | ||
}, | ||
union: (type, coll, acc, opts) => { | ||
gen.struct(type, coll, acc, opts); | ||
}, | ||
// nothing to emit directly | ||
funcptr: () => { }, | ||
}; | ||
return gen; | ||
const TYPESCRIPT = (opts = {}) => { | ||
const { indent } = { | ||
indent: " ", | ||
stringType: "slice", | ||
...opts | ||
}; | ||
const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/]; | ||
const gen = { | ||
id: "ts", | ||
pre: (_, globalOpts) => { | ||
const res = [ | ||
"// @ts-ignore possibly includes unused imports", | ||
`import { Pointer, ${__stringImpl( | ||
globalOpts | ||
)}, type MemorySlice, type WasmTypeBase, type WasmTypeConstructor } from "@thi.ng/wasm-api";` | ||
]; | ||
if (opts.pre) | ||
res.push("", ...ensureStringArray(opts.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => opts.post ? isString(opts.post) ? opts.post : opts.post.join("\n") : "", | ||
doc: (doc, acc, opts2) => { | ||
acc.push("/**", ...prefixLines(" * ", doc, opts2.lineWidth), " */"); | ||
}, | ||
enum: (e, _, acc, opts2) => { | ||
const res = []; | ||
res.push(`export enum ${e.name} {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, res, opts2); | ||
line = enumName(opts2, v.name); | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} else { | ||
line = enumName(opts2, v); | ||
} | ||
res.push(line + ","); | ||
} | ||
res.push("}", ""); | ||
acc.push(...withIndentation(res, indent, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts2) => { | ||
const strType = __stringImpl(opts2); | ||
const fields = struct.fields.map((f) => generateField(f, coll, opts2)).filter((f) => !!f); | ||
const lines = []; | ||
lines.push( | ||
`export interface ${struct.name} extends WasmTypeBase {` | ||
); | ||
for (let f of fields) { | ||
const doc = __docType(f.field, struct, coll, opts2); | ||
doc && gen.doc(doc, lines, opts2); | ||
lines.push(`${f.field.name}: ${f.type};`); | ||
} | ||
if (struct.body?.ts) { | ||
lines.push("", ...ensureLines(struct.body.ts, "decl")); | ||
} | ||
lines.push("}", ""); | ||
const pointerDecls = fields.filter((f) => isPointer(f.field.tag) && f.field.len !== 0).map((f) => { | ||
return `let $${f.field.name}: ${f.type} | null = null;`; | ||
}); | ||
const stringDecls = fields.filter( | ||
(f) => isWasmString(f.field.type) && !["array", "ptr", "slice"].includes(f.field.tag) | ||
).map((f) => `let $${f.field.name}: ${strType} | null = null;`); | ||
lines.push( | ||
`export const $${struct.name}: WasmTypeConstructor<${struct.name}> = (mem) => ({`, | ||
`get align() {`, | ||
`return ${struct.__align};`, | ||
`},`, | ||
`get size() {`, | ||
`return ${struct.__size};`, | ||
`},`, | ||
`instanceArray(base, num) {`, | ||
`const items: ${struct.name}[] = [];`, | ||
`for (; num --> 0; base += ${struct.__size}) items.push(this.instance(base));`, | ||
`return items;`, | ||
`},`, | ||
`instance: (base) => {`, | ||
...pointerDecls, | ||
...stringDecls, | ||
`return {`, | ||
`get __base() {`, | ||
`return base;`, | ||
`},`, | ||
`get __bytes() {`, | ||
`return mem.u8.subarray(base, base + ${struct.__size});`, | ||
`},` | ||
); | ||
for (let f of fields) { | ||
if (!f) | ||
continue; | ||
lines.push( | ||
`get ${f.field.name}(): ${f.type} {`, | ||
...f.getter, | ||
"}," | ||
); | ||
if (f.setter) { | ||
lines.push( | ||
`set ${f.field.name}(x: ${f.type}) {`, | ||
...f.setter, | ||
"}," | ||
); | ||
} | ||
} | ||
if (struct.body?.ts) { | ||
lines.push("", ...ensureLines(struct.body.ts, "impl"), ""); | ||
} | ||
lines.push("};", "}", "});", ""); | ||
acc.push(...withIndentation(lines, indent, ...SCOPES)); | ||
}, | ||
union: (type, coll, acc, opts2) => { | ||
gen.struct(type, coll, acc, opts2); | ||
}, | ||
// nothing to emit directly | ||
funcptr: () => { | ||
} | ||
}; | ||
return gen; | ||
}; | ||
/** @internal */ | ||
const __stringImpl = (opts) => isStringSlice(opts.stringType) ? "WasmStringSlice" : "WasmStringPtr"; | ||
/** @internal */ | ||
const __shift = (type) => BIT_SHIFTS[type]; | ||
/** @internal */ | ||
const __addr = (offset) => (offset > 0 ? `(base + ${offset})` : "base"); | ||
/** @internal */ | ||
const __addr = (offset) => offset > 0 ? `(base + ${offset})` : "base"; | ||
const __addrShift = (offset, shift) => { | ||
const bits = __shift(shift); | ||
return __addr(offset) + (bits ? " >>> " + bits : ""); | ||
const bits = __shift(shift); | ||
return __addr(offset) + (bits ? " >>> " + bits : ""); | ||
}; | ||
/** @internal */ | ||
const __ptr = (target, offset) => `mem.${target.usize}[${__addrShift(offset, target.usize)}]`; | ||
/** @internal */ | ||
const __ptrBody = (type, name, offset, body) => [ | ||
`return $${name} || ($${name} = new ${type}(mem, ${__addr(offset)},`, | ||
...body, | ||
`));`, | ||
`return $${name} || ($${name} = new ${type}(mem, ${__addr(offset)},`, | ||
...body, | ||
`));` | ||
]; | ||
const __mem = (type, offset) => `mem.${type}[${__addrShift(offset, type)}]`; | ||
/** @internal */ | ||
const __mapArray = (f, coll, len = "len") => [ | ||
`const inst = $${f.type}(mem);`, | ||
`const buf: ${f.type}[] = [];`, | ||
`for(let i = 0; i < ${len}; i++) buf.push(inst.instance(addr + i * ${coll[f.type].__size}));`, | ||
`return buf;`, | ||
`const inst = $${f.type}(mem);`, | ||
`const buf: ${f.type}[] = [];`, | ||
`for(let i = 0; i < ${len}; i++) buf.push(inst.instance(addr + i * ${coll[f.type].__size}));`, | ||
`return buf;` | ||
]; | ||
/** @internal */ | ||
const __mapStringArray = (target, name, type, len, isConst, isLocal = false) => [ | ||
isLocal ? `const $${name}: ${type}[] = [];` : `$${name} = [];`, | ||
`for(let i = 0; i < ${len}; i++) $${name}.push(new ${type}(mem, addr + i * ${target.sizeBytes * (type === "WasmStringSlice" ? 2 : 1)}, ${isConst}));`, | ||
`return $${name};`, | ||
isLocal ? `const $${name}: ${type}[] = [];` : `$${name} = [];`, | ||
`for(let i = 0; i < ${len}; i++) $${name}.push(new ${type}(mem, addr + i * ${target.sizeBytes * (type === "WasmStringSlice" ? 2 : 1)}, ${isConst}));`, | ||
`return $${name};` | ||
]; | ||
/** @internal */ | ||
const __primSlice = (type, _, __) => [ | ||
// `const addr = ${__ptrShift(opts.target, offset, type)};`, | ||
`return mem.${type}.subarray(addr, addr + len);`, | ||
// `const addr = ${__ptrShift(opts.target, offset, type)};`, | ||
`return mem.${type}.subarray(addr, addr + len);` | ||
]; | ||
/** @internal */ | ||
const __primArray = (type, len, offset) => [ | ||
`const addr = ${__addrShift(offset, type)};`, | ||
`return mem.${type}.subarray(addr, addr + ${len});`, | ||
`const addr = ${__addrShift(offset, type)};`, | ||
`return mem.${type}.subarray(addr, addr + ${len});` | ||
]; | ||
const __arrayType = (type) => isNumeric(type) | ||
? TYPEDARRAY_CTORS[type].name | ||
: BIGINT_ARRAY_CTORS[type].name; | ||
/** @internal */ | ||
const __arrayType = (type) => isNumeric(type) ? TYPEDARRAY_CTORS[type].name : BIGINT_ARRAY_CTORS[type].name; | ||
const __docType = (f, parent, coll, opts) => { | ||
const doc = [...ensureLines(f.doc || [])]; | ||
if (isPointer(f.tag) && f.len === 0) { | ||
const typeInfo = `Multi pointer: \`${zigFieldType(f, parent, coll, opts).type}\``; | ||
const remarks = "Only the pointer's target address can be accessed"; | ||
if (doc.length) { | ||
doc.push("", "@remarks", typeInfo, remarks); | ||
} | ||
else { | ||
doc.push(typeInfo, "", "@remarks", remarks); | ||
} | ||
const doc = [...ensureLines(f.doc || [])]; | ||
if (isPointer(f.tag) && f.len === 0) { | ||
const typeInfo = `Multi pointer: \`${zigFieldType(f, parent, coll, opts).type}\``; | ||
const remarks = "Only the pointer's target address can be accessed"; | ||
if (doc.length) { | ||
doc.push("", "@remarks", typeInfo, remarks); | ||
} else { | ||
doc.push(typeInfo, "", "@remarks", remarks); | ||
} | ||
else if (isWasmPrim(f.type)) { | ||
if (doc.length) | ||
doc.push("", "@remarks"); | ||
doc.push(`Zig type: \`${zigFieldType(f, parent, coll, opts).type}\``); | ||
} | ||
return doc.length ? doc : undefined; | ||
} else if (isWasmPrim(f.type)) { | ||
if (doc.length) | ||
doc.push("", "@remarks"); | ||
doc.push(`Zig type: \`${zigFieldType(f, parent, coll, opts).type}\``); | ||
} | ||
return doc.length ? doc : void 0; | ||
}; | ||
/** @internal */ | ||
const generateField = (field, coll, opts) => { | ||
if (field.skip) | ||
return; | ||
if (isFuncPointer(field.type, coll) || isOpaque(field.type)) { | ||
field = { ...field, type: opts.target.usize }; | ||
if (field.skip) | ||
return; | ||
if (isFuncPointer(field.type, coll) || isOpaque(field.type)) { | ||
field = { ...field, type: opts.target.usize }; | ||
} | ||
const { classifier, isConst } = classifyField(field, coll); | ||
const name = field.name; | ||
const offset = field.__offset; | ||
const strType = __stringImpl(opts); | ||
const isPrim = isWasmPrim(field.type); | ||
const $isEnum = isEnum(field.type, coll); | ||
let type = field.type; | ||
let decl; | ||
let getter = []; | ||
let setter; | ||
let ptrType; | ||
let tag; | ||
switch (classifier) { | ||
case "str": | ||
type = strType; | ||
getter = [ | ||
`return $${name} || ($${name} = new ${strType}(mem, ${__addr( | ||
offset | ||
)}, ${isConst}));` | ||
]; | ||
break; | ||
case "strPtr": | ||
type = `Pointer<${strType}>`; | ||
decl = `let $${name}: ${type} | null = null;`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => new ${strType}(mem, addr, ${isConst})` | ||
]); | ||
break; | ||
case "strPtrFixed": | ||
type = `Pointer<${strType}[]>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => {`, | ||
...__mapStringArray( | ||
opts.target, | ||
"buf", | ||
strType, | ||
field.len, | ||
isConst, | ||
true | ||
), | ||
"}" | ||
]); | ||
break; | ||
case "strArray": | ||
type = `${strType}[]`; | ||
getter = [ | ||
`const addr = ${__addr(offset)};`, | ||
...__mapStringArray( | ||
opts.target, | ||
name, | ||
strType, | ||
field.len, | ||
isConst, | ||
true | ||
) | ||
]; | ||
break; | ||
case "strSlice": | ||
type = `${strType}[]`; | ||
getter = [ | ||
`const addr = ${__ptr(opts.target, offset)};`, | ||
`const len = ${__ptr( | ||
opts.target, | ||
offset + opts.target.sizeBytes | ||
)};`, | ||
...__mapStringArray( | ||
opts.target, | ||
name, | ||
strType, | ||
"len", | ||
isConst, | ||
true | ||
) | ||
]; | ||
break; | ||
case "ptr": | ||
if (isPrim) { | ||
ptrType = `Pointer<number>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => mem.${type}[addr >>> ${__shift(type)}]` | ||
]); | ||
} else { | ||
ptrType = `Pointer<${type}>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => $${type}(mem).instance(addr)` | ||
]); | ||
} | ||
type = ptrType; | ||
decl = `let $${name}: ${ptrType} | null = null;`; | ||
break; | ||
case "enumPtr": | ||
tag = coll[type].tag; | ||
type = `Pointer<${type}>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => mem.${tag}[addr >>> ${__shift(tag)}]` | ||
]); | ||
break; | ||
case "ptrFixed": | ||
if (isPrim) { | ||
ptrType = `Pointer<${__arrayType(type)}>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => mem.${type}.subarray(addr, addr + ${field.len})` | ||
]); | ||
} else { | ||
ptrType = `Pointer<${type}[]>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => {`, | ||
...__mapArray(field, coll, field.len), | ||
"}" | ||
]); | ||
} | ||
type = ptrType; | ||
break; | ||
case "enumPtrFixed": | ||
tag = coll[type].tag; | ||
type = `Pointer<${__arrayType(tag)}>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => mem.${tag}.subarray(addr, addr + ${field.len})` | ||
]); | ||
break; | ||
case "array": | ||
case "vec": | ||
if (isPrim) { | ||
getter = [...__primArray(type, field.len, offset)]; | ||
type = __arrayType(type); | ||
} else { | ||
type += "[]"; | ||
getter = [ | ||
`const addr = ${__addr(offset)};`, | ||
...__mapArray(field, coll, field.len) | ||
]; | ||
} | ||
break; | ||
case "enumArray": | ||
tag = coll[type].tag; | ||
type = __arrayType(tag); | ||
getter = [...__primArray(tag, field.len, offset)]; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
getter = [ | ||
`const addr = ${__ptr(opts.target, offset)};`, | ||
`const len = ${__ptr( | ||
opts.target, | ||
offset + opts.target.sizeBytes | ||
)};` | ||
]; | ||
if (isPrim) { | ||
getter.push(...__primSlice(type, offset, opts)); | ||
type = __arrayType(type); | ||
} else if ($isEnum) { | ||
tag = coll[type].tag; | ||
getter.push(...__primSlice(tag, offset, opts)); | ||
type = __arrayType(tag); | ||
} else { | ||
type += "[]"; | ||
getter.push(...__mapArray(field, coll)); | ||
} | ||
break; | ||
case "single": | ||
if (isPrim) { | ||
getter = [`return ${__mem(type, offset)};`]; | ||
setter = [`${__mem(type, offset)} = x;`]; | ||
type = isNumeric(type) ? "number" : "bigint"; | ||
} else { | ||
getter = [`return $${type}(mem).instance(${__addr(offset)});`]; | ||
setter = [`mem.u8.set(x.__bytes, ${__addr(offset)});`]; | ||
} | ||
break; | ||
case "enum": { | ||
getter = [`return ${__mem(coll[type].tag, offset)};`]; | ||
setter = [`${__mem(coll[type].tag, offset)} = x;`]; | ||
break; | ||
} | ||
const { classifier, isConst } = classifyField(field, coll); | ||
const name = field.name; | ||
const offset = field.__offset; | ||
const strType = __stringImpl(opts); | ||
const isPrim = isWasmPrim(field.type); | ||
const $isEnum = isEnum(field.type, coll); | ||
let type = field.type; | ||
let decl; | ||
let getter = []; | ||
let setter; | ||
let ptrType; | ||
let tag; | ||
switch (classifier) { | ||
case "str": | ||
type = strType; | ||
getter = [ | ||
`return $${name} || ($${name} = new ${strType}(mem, ${__addr(offset)}, ${isConst}));`, | ||
]; | ||
break; | ||
case "strPtr": | ||
type = `Pointer<${strType}>`; | ||
decl = `let $${name}: ${type} | null = null;`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => new ${strType}(mem, addr, ${isConst})`, | ||
]); | ||
break; | ||
case "strPtrFixed": | ||
type = `Pointer<${strType}[]>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => {`, | ||
...__mapStringArray(opts.target, "buf", strType, field.len, isConst, true), | ||
"}", | ||
]); | ||
break; | ||
case "strArray": | ||
type = `${strType}[]`; | ||
getter = [ | ||
`const addr = ${__addr(offset)};`, | ||
...__mapStringArray(opts.target, name, strType, field.len, isConst, true), | ||
]; | ||
break; | ||
case "strSlice": | ||
type = `${strType}[]`; | ||
getter = [ | ||
`const addr = ${__ptr(opts.target, offset)};`, | ||
`const len = ${__ptr(opts.target, offset + opts.target.sizeBytes)};`, | ||
...__mapStringArray(opts.target, name, strType, "len", isConst, true), | ||
]; | ||
break; | ||
case "ptr": | ||
if (isPrim) { | ||
ptrType = `Pointer<number>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => mem.${type}[addr >>> ${__shift(type)}]`, | ||
]); | ||
} | ||
else { | ||
ptrType = `Pointer<${type}>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => $${type}(mem).instance(addr)`, | ||
]); | ||
} | ||
type = ptrType; | ||
decl = `let $${name}: ${ptrType} | null = null;`; | ||
break; | ||
case "enumPtr": | ||
tag = coll[type].tag; | ||
type = `Pointer<${type}>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => mem.${tag}[addr >>> ${__shift(tag)}]`, | ||
]); | ||
break; | ||
case "ptrFixed": | ||
if (isPrim) { | ||
ptrType = `Pointer<${__arrayType(type)}>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => mem.${type}.subarray(addr, addr + ${field.len})`, | ||
]); | ||
} | ||
else { | ||
ptrType = `Pointer<${type}[]>`; | ||
getter = __ptrBody(ptrType, name, offset, [ | ||
`(addr) => {`, | ||
...__mapArray(field, coll, field.len), | ||
"}", | ||
]); | ||
} | ||
type = ptrType; | ||
break; | ||
case "enumPtrFixed": | ||
tag = coll[type].tag; | ||
type = `Pointer<${__arrayType(tag)}>`; | ||
getter = __ptrBody(type, name, offset, [ | ||
`(addr) => mem.${tag}.subarray(addr, addr + ${field.len})`, | ||
]); | ||
break; | ||
case "array": | ||
case "vec": | ||
if (isPrim) { | ||
getter = [...__primArray(type, field.len, offset)]; | ||
type = __arrayType(type); | ||
} | ||
else { | ||
type += "[]"; | ||
getter = [ | ||
`const addr = ${__addr(offset)};`, | ||
...__mapArray(field, coll, field.len), | ||
]; | ||
} | ||
break; | ||
case "enumArray": | ||
tag = coll[type].tag; | ||
type = __arrayType(tag); | ||
getter = [...__primArray(tag, field.len, offset)]; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
getter = [ | ||
`const addr = ${__ptr(opts.target, offset)};`, | ||
`const len = ${__ptr(opts.target, offset + opts.target.sizeBytes)};`, | ||
]; | ||
if (isPrim) { | ||
getter.push(...__primSlice(type, offset, opts)); | ||
type = __arrayType(type); | ||
} | ||
else if ($isEnum) { | ||
tag = coll[type].tag; | ||
getter.push(...__primSlice(tag, offset, opts)); | ||
type = __arrayType(tag); | ||
} | ||
else { | ||
type += "[]"; | ||
getter.push(...__mapArray(field, coll)); | ||
} | ||
break; | ||
case "single": | ||
if (isPrim) { | ||
getter = [`return ${__mem(type, offset)};`]; | ||
setter = [`${__mem(type, offset)} = x;`]; | ||
type = isNumeric(type) ? "number" : "bigint"; | ||
} | ||
else { | ||
getter = [`return $${type}(mem).instance(${__addr(offset)});`]; | ||
setter = [`mem.u8.set(x.__bytes, ${__addr(offset)});`]; | ||
} | ||
break; | ||
case "enum": { | ||
getter = [`return ${__mem(coll[type].tag, offset)};`]; | ||
setter = [`${__mem(coll[type].tag, offset)} = x;`]; | ||
break; | ||
} | ||
case "ptrMulti": | ||
case "enumPtrMulti": | ||
case "strPtrMulti": | ||
// impossible to map memory of undefined length so only return | ||
// pointer's target addr (same as `opaquePtr`) | ||
type = "number"; | ||
getter = [`return ${__ptr(opts.target, offset)};`]; | ||
setter = [`${__ptr(opts.target, offset)} = x;`]; | ||
break; | ||
case "pad": | ||
// skip codegen for padding fields and multi pointers since | ||
return; | ||
default: | ||
unsupported(`TODO: ${classifier} - please report as issue`); | ||
} | ||
return { field, type, decl, getter, setter }; | ||
case "ptrMulti": | ||
case "enumPtrMulti": | ||
case "strPtrMulti": | ||
type = "number"; | ||
getter = [`return ${__ptr(opts.target, offset)};`]; | ||
setter = [`${__ptr(opts.target, offset)} = x;`]; | ||
break; | ||
case "pad": | ||
return; | ||
default: | ||
unsupported(`TODO: ${classifier} - please report as issue`); | ||
} | ||
return { field, type, decl, getter, setter }; | ||
}; | ||
export { | ||
TYPESCRIPT | ||
}; |
425
zig.js
@@ -6,212 +6,235 @@ import { isNumber } from "@thi.ng/checks"; | ||
import { classifyField } from "./internal/classify.js"; | ||
import { defaultValue, ensureLines, ensureStringArray, isOpaque, isPadding, isStringSlice, isUnion, isWasmString, prefixLines, sliceTypes, withIndentation, } from "./internal/utils.js"; | ||
/** | ||
* Zig code generator. Call with options and then pass to {@link generateTypes} | ||
* (see its docs for further usage). | ||
* | ||
* @remarks | ||
* This codegen generates struct and enum definitions for a {@link TypeColl} | ||
* given to {@link generateTypes}. | ||
* | ||
* @param opts | ||
*/ | ||
export const ZIG = (opts = {}) => { | ||
const INDENT = " "; | ||
const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/]; | ||
const gen = { | ||
id: "zig", | ||
pre: (coll) => { | ||
const res = [ | ||
`const std = @import("std");`, | ||
`const bindgen = @import("wasm-api-bindgen");`, | ||
]; | ||
for (let type of sliceTypes(coll)) { | ||
if (type !== "string" && type !== "opaque") { | ||
const name = capitalize(type); | ||
res.push(`\npub const ${name}Slice = bindgen.Slice([]${type}, [*]${type});`, `pub const Const${name}Slice = bindgen.Slice([]const ${type}, [*]const ${type});`); | ||
} | ||
} | ||
if (opts.pre) | ||
res.push("", ...ensureStringArray(opts.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => opts.post | ||
? isString(opts.post) | ||
? opts.post | ||
: opts.post.join("\n") | ||
: "", | ||
doc: (doc, acc, opts, topLevel = false) => { | ||
acc.push(...prefixLines(topLevel ? "//! " : "/// ", doc, opts.lineWidth)); | ||
}, | ||
enum: (e, _, acc, opts) => { | ||
const lines = []; | ||
lines.push(`pub const ${e.name} = enum(${e.tag}) {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, lines, opts); | ||
line = v.name; | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} | ||
else { | ||
line = v; | ||
} | ||
lines.push(line + ","); | ||
} | ||
if (e.body?.zig) { | ||
lines.push("", ...ensureLines(e.body.zig, "impl"), ""); | ||
} | ||
lines.push("};", ""); | ||
acc.push(...withIndentation(lines, INDENT, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts) => { | ||
acc.push(...withIndentation([ | ||
`pub const ${struct.name} = extern struct {`, | ||
...__generateFields(gen, struct, coll, opts), | ||
], INDENT, ...SCOPES)); | ||
}, | ||
union: (union, coll, acc, opts) => { | ||
acc.push(...withIndentation([ | ||
`pub const ${union.name} = extern union {`, | ||
...__generateFields(gen, union, coll, opts), | ||
], INDENT, ...SCOPES)); | ||
}, | ||
funcptr: (ptr, coll, acc, opts) => { | ||
const args = ptr.args | ||
.map((a) => `${a.name}: ${fieldType(a, ptr, coll, opts).type}`) | ||
.join(", "); | ||
const rtype = ptr.rtype === "void" | ||
? ptr.rtype | ||
: fieldType({ name: "return", ...ptr.rtype }, ptr, coll, opts).type; | ||
acc.push(`pub const ${ptr.name} = *const fn (${args}) ${rtype};`, ""); | ||
}, | ||
}; | ||
return gen; | ||
import { | ||
defaultValue, | ||
ensureLines, | ||
ensureStringArray, | ||
isOpaque, | ||
isPadding, | ||
isStringSlice, | ||
isUnion, | ||
isWasmString, | ||
prefixLines, | ||
sliceTypes, | ||
withIndentation | ||
} from "./internal/utils.js"; | ||
const ZIG = (opts = {}) => { | ||
const INDENT = " "; | ||
const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/]; | ||
const gen = { | ||
id: "zig", | ||
pre: (coll) => { | ||
const res = [ | ||
`const std = @import("std");`, | ||
`const bindgen = @import("wasm-api-bindgen");` | ||
]; | ||
for (let type of sliceTypes(coll)) { | ||
if (type !== "string" && type !== "opaque") { | ||
const name = capitalize(type); | ||
res.push( | ||
` | ||
pub const ${name}Slice = bindgen.Slice([]${type}, [*]${type});`, | ||
`pub const Const${name}Slice = bindgen.Slice([]const ${type}, [*]const ${type});` | ||
); | ||
} | ||
} | ||
if (opts.pre) | ||
res.push("", ...ensureStringArray(opts.pre)); | ||
return res.join("\n"); | ||
}, | ||
post: () => opts.post ? isString(opts.post) ? opts.post : opts.post.join("\n") : "", | ||
doc: (doc, acc, opts2, topLevel = false) => { | ||
acc.push( | ||
...prefixLines(topLevel ? "//! " : "/// ", doc, opts2.lineWidth) | ||
); | ||
}, | ||
enum: (e, _, acc, opts2) => { | ||
const lines = []; | ||
lines.push(`pub const ${e.name} = enum(${e.tag}) {`); | ||
for (let v of e.values) { | ||
let line; | ||
if (!isString(v)) { | ||
v.doc && gen.doc(v.doc, lines, opts2); | ||
line = v.name; | ||
if (v.value != null) | ||
line += ` = ${v.value}`; | ||
} else { | ||
line = v; | ||
} | ||
lines.push(line + ","); | ||
} | ||
if (e.body?.zig) { | ||
lines.push("", ...ensureLines(e.body.zig, "impl"), ""); | ||
} | ||
lines.push("};", ""); | ||
acc.push(...withIndentation(lines, INDENT, ...SCOPES)); | ||
}, | ||
struct: (struct, coll, acc, opts2) => { | ||
acc.push( | ||
...withIndentation( | ||
[ | ||
`pub const ${struct.name} = extern struct {`, | ||
...__generateFields(gen, struct, coll, opts2) | ||
], | ||
INDENT, | ||
...SCOPES | ||
) | ||
); | ||
}, | ||
union: (union, coll, acc, opts2) => { | ||
acc.push( | ||
...withIndentation( | ||
[ | ||
`pub const ${union.name} = extern union {`, | ||
...__generateFields(gen, union, coll, opts2) | ||
], | ||
INDENT, | ||
...SCOPES | ||
) | ||
); | ||
}, | ||
funcptr: (ptr, coll, acc, opts2) => { | ||
const args = ptr.args.map((a) => `${a.name}: ${fieldType(a, ptr, coll, opts2).type}`).join(", "); | ||
const rtype = ptr.rtype === "void" ? ptr.rtype : fieldType( | ||
{ name: "return", ...ptr.rtype }, | ||
ptr, | ||
coll, | ||
opts2 | ||
).type; | ||
acc.push( | ||
`pub const ${ptr.name} = *const fn (${args}) ${rtype};`, | ||
"" | ||
); | ||
} | ||
}; | ||
return gen; | ||
}; | ||
const __generateFields = (gen, parent, coll, opts) => { | ||
const res = []; | ||
const ftypes = {}; | ||
const name = parent.name; | ||
let padID = 0; | ||
const res = []; | ||
const ftypes = {}; | ||
const name = parent.name; | ||
let padID = 0; | ||
for (let f of parent.fields) { | ||
if (isPadding(f)) { | ||
res.push(`__pad${padID}: [${f.pad}]u8,`); | ||
padID++; | ||
continue; | ||
} | ||
f.doc && gen.doc(f.doc, res, opts); | ||
const { type, defaultVal } = fieldType(f, parent, coll, opts); | ||
ftypes[f.name] = type; | ||
res.push(`${f.name}: ${type}${defaultVal},`); | ||
} | ||
if (parent.body?.zig) { | ||
res.push("", ...ensureLines(parent.body.zig, "impl"), ""); | ||
} | ||
res.push("};"); | ||
if (opts.debug) { | ||
const fn = (fname, body) => res.push( | ||
"", | ||
`export fn ${name}_${fname}() usize {`, | ||
`return ${body};`, | ||
`}` | ||
); | ||
fn("align", `@alignOf(${name})`); | ||
fn("size", `@sizeOf(${name})`); | ||
for (let f of parent.fields) { | ||
// autolabel explicit padding fields | ||
if (isPadding(f)) { | ||
res.push(`__pad${padID}: [${f.pad}]u8,`); | ||
padID++; | ||
continue; | ||
} | ||
f.doc && gen.doc(f.doc, res, opts); | ||
const { type, defaultVal } = fieldType(f, parent, coll, opts); | ||
ftypes[f.name] = type; | ||
res.push(`${f.name}: ${type}${defaultVal},`); | ||
if (isPadding(f)) | ||
continue; | ||
fn(f.name + "_align", `@alignOf(${ftypes[f.name]})`); | ||
!isUnion(parent) && fn(f.name + "_offset", `@offsetOf(${name}, "${f.name}")`); | ||
fn(f.name + "_size", `@sizeOf(${ftypes[f.name]})`); | ||
} | ||
if (parent.body?.zig) { | ||
res.push("", ...ensureLines(parent.body.zig, "impl"), ""); | ||
} | ||
res.push("};"); | ||
if (opts.debug) { | ||
const fn = (fname, body) => res.push("", `export fn ${name}_${fname}() usize {`, `return ${body};`, `}`); | ||
fn("align", `@alignOf(${name})`); | ||
fn("size", `@sizeOf(${name})`); | ||
for (let f of parent.fields) { | ||
if (isPadding(f)) | ||
continue; | ||
fn(f.name + "_align", `@alignOf(${ftypes[f.name]})`); | ||
!isUnion(parent) && | ||
fn(f.name + "_offset", `@offsetOf(${name}, "${f.name}")`); | ||
fn(f.name + "_size", `@sizeOf(${ftypes[f.name]})`); | ||
} | ||
} | ||
res.push(""); | ||
return res; | ||
} | ||
res.push(""); | ||
return res; | ||
}; | ||
/** @internal */ | ||
export const fieldType = (f, parent, coll, opts) => { | ||
let type = f.type; | ||
let defaultVal = defaultValue(f, "zig"); | ||
const { classifier, isConst } = classifyField(f, coll); | ||
const $isConst = isConst ? "Const" : ""; | ||
if (isWasmString(f.type)) { | ||
type = isStringSlice(opts.stringType) | ||
? `bindgen.${$isConst}String` | ||
: `bindgen.${$isConst}StringPtr`; | ||
switch (classifier) { | ||
case "strPtr": | ||
type = `*${type}`; | ||
break; | ||
case "strPtrFixed": | ||
type = `*[${f.len}]${type}`; | ||
break; | ||
case "strPtrMulti": | ||
type = `[*]${type}`; | ||
break; | ||
case "strSlice": | ||
type += "Slice"; | ||
break; | ||
case "strArray": | ||
type = `[${f.len}]${type}`; | ||
break; | ||
} | ||
const fieldType = (f, parent, coll, opts) => { | ||
let type = f.type; | ||
let defaultVal = defaultValue(f, "zig"); | ||
const { classifier, isConst } = classifyField(f, coll); | ||
const $isConst = isConst ? "Const" : ""; | ||
if (isWasmString(f.type)) { | ||
type = isStringSlice(opts.stringType) ? `bindgen.${$isConst}String` : `bindgen.${$isConst}StringPtr`; | ||
switch (classifier) { | ||
case "strPtr": | ||
type = `*${type}`; | ||
break; | ||
case "strPtrFixed": | ||
type = `*[${f.len}]${type}`; | ||
break; | ||
case "strPtrMulti": | ||
type = `[*]${type}`; | ||
break; | ||
case "strSlice": | ||
type += "Slice"; | ||
break; | ||
case "strArray": | ||
type = `[${f.len}]${type}`; | ||
break; | ||
} | ||
else if (isOpaque(f.type)) { | ||
type = `bindgen.${$isConst}OpaquePtr`; | ||
switch (classifier) { | ||
case "opaquePtr": | ||
type = `*${type}`; | ||
break; | ||
case "opaquePtrFixed": | ||
type = `*[${f.len}]${type}`; | ||
break; | ||
case "opaquePtrMulti": | ||
type = `[*]${type}`; | ||
break; | ||
case "opaqueSlice": | ||
type += "Slice"; | ||
break; | ||
case "opaqueArray": | ||
type = `[${f.len}]${type}`; | ||
break; | ||
} | ||
} else if (isOpaque(f.type)) { | ||
type = `bindgen.${$isConst}OpaquePtr`; | ||
switch (classifier) { | ||
case "opaquePtr": | ||
type = `*${type}`; | ||
break; | ||
case "opaquePtrFixed": | ||
type = `*[${f.len}]${type}`; | ||
break; | ||
case "opaquePtrMulti": | ||
type = `[*]${type}`; | ||
break; | ||
case "opaqueSlice": | ||
type += "Slice"; | ||
break; | ||
case "opaqueArray": | ||
type = `[${f.len}]${type}`; | ||
break; | ||
} | ||
else { | ||
const $const = isConst ? "const " : ""; | ||
const sentinel = f.sentinel != null ? `:${f.sentinel}` : ""; | ||
switch (classifier) { | ||
case "ptr": | ||
case "enumPtr": | ||
type = `*${$const}${type}`; | ||
break; | ||
case "ptrFixed": | ||
case "enumPtrFixed": | ||
type = `*${$const}[${f.len}${sentinel}]${type}`; | ||
break; | ||
case "ptrMulti": | ||
case "enumPtrMulti": | ||
type = `[*${sentinel}]${$const}${type}`; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
type = `${$isConst}${capitalize(f.type)}Slice`; | ||
break; | ||
case "array": | ||
case "enumArray": | ||
type = `[${f.len}${sentinel}]${type}`; | ||
break; | ||
case "vec": | ||
type = `@Vector(${f.len}, ${type})`; | ||
break; | ||
} | ||
} else { | ||
const $const = isConst ? "const " : ""; | ||
const sentinel = f.sentinel != null ? `:${f.sentinel}` : ""; | ||
switch (classifier) { | ||
case "ptr": | ||
case "enumPtr": | ||
type = `*${$const}${type}`; | ||
break; | ||
case "ptrFixed": | ||
case "enumPtrFixed": | ||
type = `*${$const}[${f.len}${sentinel}]${type}`; | ||
break; | ||
case "ptrMulti": | ||
case "enumPtrMulti": | ||
type = `[*${sentinel}]${$const}${type}`; | ||
break; | ||
case "slice": | ||
case "enumSlice": | ||
type = `${$isConst}${capitalize(f.type)}Slice`; | ||
break; | ||
case "array": | ||
case "enumArray": | ||
type = `[${f.len}${sentinel}]${type}`; | ||
break; | ||
case "vec": | ||
type = `@Vector(${f.len}, ${type})`; | ||
break; | ||
} | ||
if (f.optional) | ||
type = "?" + type; | ||
if (defaultVal != undefined) { | ||
if (!(isString(defaultVal) || isNumber(defaultVal))) { | ||
illegalArgs(`wrong default value for ${parent.name}.${f.name} (${defaultVal})`); | ||
} | ||
} | ||
if (f.optional) | ||
type = "?" + type; | ||
if (defaultVal != void 0) { | ||
if (!(isString(defaultVal) || isNumber(defaultVal))) { | ||
illegalArgs( | ||
`wrong default value for ${parent.name}.${f.name} (${defaultVal})` | ||
); | ||
} | ||
return { | ||
type, | ||
defaultVal: defaultVal != undefined ? ` = ${defaultVal}` : "", | ||
}; | ||
} | ||
return { | ||
type, | ||
defaultVal: defaultVal != void 0 ? ` = ${defaultVal}` : "" | ||
}; | ||
}; | ||
export { | ||
ZIG, | ||
fieldType | ||
}; |
2453
126490
6
Updated@thi.ng/api@^8.9.12
Updated@thi.ng/args@^2.2.46
Updated@thi.ng/arrays@^2.7.8
Updated@thi.ng/binary@^3.4.0
Updated@thi.ng/checks@^3.4.12
Updated@thi.ng/compare@^2.2.8
Updated@thi.ng/defmulti@^3.0.10
Updated@thi.ng/errors@^2.4.6
Updated@thi.ng/file-io@^1.0.5
Updated@thi.ng/logger@^2.0.2
Updated@thi.ng/paths@^5.1.53
Updated@thi.ng/strings@^3.7.3
Updated@thi.ng/wasm-api@^1.4.37