diff --git a/.gitignore b/.gitignore
index f9621c1c7..5fb797eca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,7 @@ reach_wrap_output.json
**/resolve_log.out*
*.facts
-*.facts.zst
\ No newline at end of file
+*.facts.zst
+
+# rust
+*target*
diff --git a/docs/development/building-from-source.md b/docs/development/building-from-source.md
index ca7c5e86f..fe855bece 100644
--- a/docs/development/building-from-source.md
+++ b/docs/development/building-from-source.md
@@ -3,6 +3,7 @@
**RESOLVE** has been tested on **Ubuntu 24.04.4 LTS**, but should work on other distributions that can provide the following packages:
- Nightly Rust
+- cbindgen (`cargo install cbindgen --version 0.29.0 --locked`)
- uv
- CMake
- build-essential
diff --git a/resolve-facts/CMakeLists.txt b/resolve-facts/CMakeLists.txt
index aa11efe9c..abfb69336 100644
--- a/resolve-facts/CMakeLists.txt
+++ b/resolve-facts/CMakeLists.txt
@@ -13,6 +13,44 @@ endif()
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
+######################################################################
+# RUST FACTS ABI
+
+set(FACTS_RS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rs")
+set(FACTS_RS_TARGET_DIR "${FACTS_RS_DIR}/target/release")
+set(FACTS_RS_STATICLIB "${FACTS_RS_TARGET_DIR}/libfacts_rs.a")
+set(FACTS_RS_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
+set(FACTS_RS_HEADER "${FACTS_RS_GENERATED_DIR}/facts_rs.hpp")
+file(MAKE_DIRECTORY "${FACTS_RS_GENERATED_DIR}")
+
+find_program(CARGO_EXECUTABLE cargo REQUIRED)
+find_program(CBINDGEN_EXECUTABLE cbindgen HINTS "$ENV{HOME}/.cargo/bin" REQUIRED)
+
+file(GLOB FACTS_RS_SOURCES CONFIGURE_DEPENDS "${FACTS_RS_DIR}/src/*.rs")
+add_custom_command(
+ OUTPUT "${FACTS_RS_STATICLIB}" "${FACTS_RS_HEADER}"
+ COMMAND "${CMAKE_COMMAND}" -E make_directory "${FACTS_RS_GENERATED_DIR}"
+ COMMAND "${CARGO_EXECUTABLE}" build --manifest-path "${FACTS_RS_DIR}/Cargo.toml" --release
+ COMMAND "${CBINDGEN_EXECUTABLE}" --config "${FACTS_RS_DIR}/cbindgen.toml"
+ --crate facts-rs --output "${FACTS_RS_HEADER}" "${FACTS_RS_DIR}"
+ DEPENDS ${FACTS_RS_SOURCES} "${FACTS_RS_DIR}/Cargo.toml" "${FACTS_RS_DIR}/Cargo.lock"
+ "${FACTS_RS_DIR}/cbindgen.toml"
+ WORKING_DIRECTORY "${FACTS_RS_DIR}"
+ COMMENT "Building facts-rs and generating its C++ ABI header"
+ VERBATIM
+)
+add_custom_target(facts_rs_build ALL DEPENDS "${FACTS_RS_STATICLIB}" "${FACTS_RS_HEADER}")
+
+add_library(facts_rs STATIC IMPORTED GLOBAL)
+set_target_properties(facts_rs PROPERTIES
+ IMPORTED_LOCATION "${FACTS_RS_STATICLIB}"
+ INTERFACE_INCLUDE_DIRECTORIES "${FACTS_RS_GENERATED_DIR}"
+)
+add_dependencies(facts_rs facts_rs_build)
+
+install(FILES "${FACTS_RS_STATICLIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
+install(FILES "${FACTS_RS_HEADER}" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
+
add_subdirectory(vendor/argparse)
add_subdirectory(vendor/json)
diff --git a/resolve-facts/Config.cmake.in b/resolve-facts/Config.cmake.in
index 6909bc4fb..9ca79c112 100644
--- a/resolve-facts/Config.cmake.in
+++ b/resolve-facts/Config.cmake.in
@@ -1,9 +1,16 @@
@PACKAGE_INIT@
-include("${CMAKE_CURRENT_LIST_DIR}/ResolveFactsTargets.cmake")
-
include(CMakeFindDependencyMacro)
find_dependency(glaze)
+
+if(NOT TARGET facts_rs)
+ add_library(facts_rs STATIC IMPORTED)
+ set_target_properties(facts_rs PROPERTIES
+ IMPORTED_LOCATION "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/libfacts_rs.a"
+ INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@"
+ )
+endif()
+
include("${CMAKE_CURRENT_LIST_DIR}/ResolveFactsTargets.cmake")
check_required_components(ResolveFacts)
diff --git a/resolve-facts/README.md b/resolve-facts/README.md
index bbd34d7e1..c3e72624f 100644
--- a/resolve-facts/README.md
+++ b/resolve-facts/README.md
@@ -11,3 +11,7 @@ Tools for creating and querying RESOLVE binary metadata, including the `reach` t
- Facts:
- `reach` tool:
+
+## Future Improvements
+
+Strings are interned at an LLVM Module-level. If we were to intern strings across every module together, we would be able to get more space savings, but likely at the expense of more CPU-heavy assembly/decompression. It's also unclear if/how we could compress ELF strings inline.
diff --git a/resolve-facts/rs/Cargo.lock b/resolve-facts/rs/Cargo.lock
new file mode 100644
index 000000000..f95a5febc
--- /dev/null
+++ b/resolve-facts/rs/Cargo.lock
@@ -0,0 +1,65 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "facts-rs"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/resolve-facts/rs/Cargo.toml b/resolve-facts/rs/Cargo.toml
new file mode 100644
index 000000000..725335a9f
--- /dev/null
+++ b/resolve-facts/rs/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "facts-rs"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["rlib", "staticlib", "cdylib"]
+
+[dependencies]
+bytemuck = { version = "1", features = ["derive"] }
diff --git a/resolve-facts/rs/cbindgen.toml b/resolve-facts/rs/cbindgen.toml
new file mode 100644
index 000000000..babadc1ac
--- /dev/null
+++ b/resolve-facts/rs/cbindgen.toml
@@ -0,0 +1,21 @@
+language = "C++"
+namespace = "facts_rs"
+pragma_once = true
+include_guard = "RESOLVE_FACTS_RS_H"
+header = """
+/*
+ * Generated by cbindgen from resolve-facts/rs. Do not edit by hand.
+ */
+"""
+
+[parse]
+parse_deps = false
+
+[parse.expand]
+crates = ["facts-rs"]
+
+[export]
+include = [
+ "Node",
+ "Edge",
+]
diff --git a/resolve-facts/rs/src/builder.rs b/resolve-facts/rs/src/builder.rs
new file mode 100644
index 000000000..aff4d9621
--- /dev/null
+++ b/resolve-facts/rs/src/builder.rs
@@ -0,0 +1,377 @@
+use std::collections::*;
+use std::mem::*;
+
+use crate::interner::*;
+use crate::schema::*;
+use crate::utils::*;
+use crate::writer::*;
+
+pub type ModuleHandle = u32;
+pub const INVALID_ID: u32 = u32::MAX;
+
+struct ModuleBuilder {
+ nodes: Vec,
+ edges: Vec,
+ pool: Interner,
+ edge_indexes: HashMap<(NodeID, NodeID), usize>,
+}
+
+impl ModuleBuilder {
+ fn new(hint: usize) -> Self {
+ let edge_hint = hint.saturating_mul(2);
+ Self {
+ nodes: Vec::with_capacity(hint),
+ edges: Vec::with_capacity(edge_hint),
+ pool: Interner::default(),
+ edge_indexes: HashMap::with_capacity(edge_hint),
+ }
+ }
+
+ fn add_node(&mut self, ty: NodeType) -> NodeID {
+ let dense_id = u32::try_from(self.nodes.len()).expect("module has too many nodes");
+ self.nodes.push(Node::new(ty));
+ dense_id
+ }
+
+ fn node_mut(&mut self, id: NodeID) -> Option<&mut Node> {
+ self.nodes.get_mut(id as usize)
+ }
+
+ fn add_edge(&mut self, src: NodeID, dst: NodeID, kind: EdgeKind) -> bool {
+ if src as usize >= self.nodes.len() || dst as usize >= self.nodes.len() {
+ return false;
+ }
+
+ let id = (src, dst);
+ let kind = 1u32 << kind as u8;
+ if let Some(&index) = self.edge_indexes.get(&id) {
+ self.edges[index].kinds |= kind;
+ return true;
+ }
+
+ self.edge_indexes.insert(id, self.edges.len());
+ self.edges.push(Edge {
+ src,
+ dst,
+ kinds: kind,
+ });
+ true
+ }
+
+ fn intern_for_node(&mut self, id: NodeID, value: &str) -> Option<(NodeID, Interned)> {
+ self.nodes.get(id as usize)?;
+ let interned = self.pool.intern(value);
+ Some((id, interned))
+ }
+
+ fn word_len(&self) -> usize {
+ size_of::() / size_of::()
+ + self.nodes.len() * size_of::() / size_of::()
+ + self.edges.len() * size_of::() / size_of::()
+ + self.pool.bytes().len().div_ceil(size_of::())
+ }
+
+ fn serialize_into(mut self, words: &mut Vec) {
+ assert!(
+ self.nodes
+ .first()
+ .is_some_and(|node| node.node_type_raw() == NodeType::Module as u8),
+ "module node 0 must have type Module"
+ );
+ let pool_len = self.pool.bytes().len();
+ let padded_pool_len = pool_len.checked_add(3).expect("intern pool is too large") & !3;
+ let header = ModuleHeader {
+ version: FORMAT_VERSION,
+ node_count: u32::try_from(self.nodes.len()).expect("module has too many nodes"),
+ edge_count: u32::try_from(self.edges.len()).expect("module has too many edges"),
+ string_pool_len: u32::try_from(padded_pool_len)
+ .expect("module intern pool exceeds 4 GiB"),
+ };
+
+ words.extend_from_slice(bytemuck::cast_slice(std::slice::from_ref(&header)));
+ words.extend_from_slice(bytemuck::cast_slice(&self.nodes));
+
+ self.edges.sort_unstable_by_key(|edge| (edge.src, edge.dst));
+ words.extend_from_slice(bytemuck::cast_slice(&self.edges));
+
+ for chunk in self.pool.bytes().chunks(size_of::()) {
+ let mut bytes = [0; size_of::()];
+ bytes[..chunk.len()].copy_from_slice(chunk);
+ words.push(u32::from_ne_bytes(bytes));
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct FactsBuilder {
+ modules: Vec,
+}
+
+impl FactsBuilder {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn add_module(&mut self, hint: usize) -> ModuleHandle {
+ let index = u32::try_from(self.modules.len()).expect("program has too many modules");
+ self.modules.push(ModuleBuilder::new(hint));
+ index
+ }
+
+ pub fn add_node(&mut self, module: ModuleHandle, ty: NodeType) -> Option {
+ Some(self.module_mut(module)?.add_node(ty))
+ }
+
+ pub fn add_edge(
+ &mut self,
+ module: ModuleHandle,
+ src: NodeID,
+ dst: NodeID,
+ kind: EdgeKind,
+ ) -> bool {
+ self.module_mut(module)
+ .is_some_and(|module| module.add_edge(src, dst, kind))
+ }
+
+ pub fn set_node_idx(&mut self, module: ModuleHandle, node: NodeID, value: u32) -> bool {
+ self.set_node(module, node, |node| {
+ node.idx = value;
+ node.set_present(P_IDX);
+ })
+ }
+
+ pub fn set_node_name(&mut self, module: ModuleHandle, node: NodeID, value: &str) -> bool {
+ self.set_node_string(module, node, value, |node, value| {
+ node.name = value;
+ node.set_present(P_NAME);
+ })
+ }
+
+ pub fn set_node_opcode(&mut self, module: ModuleHandle, node: NodeID, value: &str) -> bool {
+ self.set_node_string(module, node, value, |node, value| {
+ node.opcode = value;
+ node.set_present(P_OPCODE);
+ })
+ }
+
+ pub fn set_node_linkage(&mut self, module: ModuleHandle, node: NodeID, value: Linkage) -> bool {
+ self.set_node(module, node, |node| node.set_linkage(value))
+ }
+
+ pub fn set_node_call_type(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ value: CallType,
+ ) -> bool {
+ self.set_node(module, node, |node| node.set_call_type(value))
+ }
+
+ pub fn set_node_source_loc(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ line: u32,
+ col: u32,
+ ) -> bool {
+ self.set_node(module, node, |node| {
+ node.source_line = line;
+ node.source_col = col;
+ node.set_present(P_SOURCE_LOC);
+ })
+ }
+
+ pub fn set_node_source_file(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ value: &str,
+ ) -> bool {
+ self.set_node_string(module, node, value, |node, value| {
+ node.source_file = value;
+ node.set_present(P_SOURCE_FILE);
+ })
+ }
+
+ pub fn set_node_function_type(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ value: &str,
+ ) -> bool {
+ self.set_node_string(module, node, value, |node, value| {
+ node.function_type = value;
+ node.set_present(P_FUNCTION_TYPE);
+ })
+ }
+
+ pub fn set_node_address_taken(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ value: bool,
+ ) -> bool {
+ self.set_node(module, node, |node| {
+ if value {
+ node.set_present(P_ADDRESS_TAKEN);
+ } else {
+ node.meta &= !P_ADDRESS_TAKEN;
+ }
+ })
+ }
+
+ pub fn freeze(self) -> FactsBuf {
+ let capacity = self.modules.iter().map(ModuleBuilder::word_len).sum();
+ let mut words = Vec::with_capacity(capacity);
+ for module in self.modules {
+ module.serialize_into(&mut words);
+ }
+
+ FactsBuf::from_words(words)
+ }
+
+ fn module_mut(&mut self, module: ModuleHandle) -> Option<&mut ModuleBuilder> {
+ self.modules.get_mut(module as usize)
+ }
+
+ fn set_node(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ update: impl FnOnce(&mut Node),
+ ) -> bool {
+ let Some(node) = self
+ .module_mut(module)
+ .and_then(|module| module.node_mut(node))
+ else {
+ return false;
+ };
+ update(node);
+ true
+ }
+
+ fn set_node_string(
+ &mut self,
+ module: ModuleHandle,
+ node: NodeID,
+ value: &str,
+ update: impl FnOnce(&mut Node, Interned),
+ ) -> bool {
+ let Some(module) = self.module_mut(module) else {
+ return false;
+ };
+ let Some((node, interned)) = module.intern_for_node(node, value) else {
+ return false;
+ };
+ update(&mut module.nodes[node as usize], interned);
+ true
+ }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_new() -> *mut FactsBuilder {
+ Box::into_raw(Box::new(FactsBuilder::new()))
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_free(builder: *mut FactsBuilder) {
+ if !builder.is_null() {
+ unsafe {
+ drop(Box::from_raw(builder));
+ }
+ }
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_add_module(
+ builder: *mut FactsBuilder,
+ hint: usize,
+) -> ModuleHandle {
+ unsafe { builder.as_mut() }.map_or(INVALID_ID, |builder| builder.add_module(hint))
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_add_node(
+ builder: *mut FactsBuilder,
+ module: ModuleHandle,
+ ty: NodeType,
+) -> NodeID {
+ unsafe { builder.as_mut() }
+ .and_then(|builder| builder.add_node(module, ty))
+ .unwrap_or(INVALID_ID)
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_add_edge(
+ builder: *mut FactsBuilder,
+ module: ModuleHandle,
+ src: NodeID,
+ dst: NodeID,
+ kind: EdgeKind,
+) -> bool {
+ unsafe { builder.as_mut() }.is_some_and(|builder| builder.add_edge(module, src, dst, kind))
+}
+
+macro_rules! ffi_node_setter {
+ ($ffi:ident, $method:ident, $ty:ty) => {
+ #[unsafe(no_mangle)]
+ pub extern "C" fn $ffi(
+ builder: *mut FactsBuilder,
+ module: ModuleHandle,
+ node: NodeID,
+ value: $ty,
+ ) -> bool {
+ unsafe { builder.as_mut() }.is_some_and(|builder| builder.$method(module, node, value))
+ }
+ };
+}
+
+macro_rules! ffi_node_string_setter {
+ ($ffi:ident, $method:ident) => {
+ #[unsafe(no_mangle)]
+ pub extern "C" fn $ffi(
+ builder: *mut FactsBuilder,
+ module: ModuleHandle,
+ node: NodeID,
+ ptr: *const u8,
+ len: usize,
+ ) -> bool {
+ let Some(builder) = (unsafe { builder.as_mut() }) else {
+ return false;
+ };
+ let Some(value) = (unsafe { as_str(ptr, len) }) else {
+ return false;
+ };
+ builder.$method(module, node, value)
+ }
+ };
+}
+
+ffi_node_setter!(facts_builder_set_node_idx, set_node_idx, u32);
+ffi_node_string_setter!(facts_builder_set_node_name, set_node_name);
+ffi_node_string_setter!(facts_builder_set_node_opcode, set_node_opcode);
+ffi_node_setter!(facts_builder_set_node_linkage, set_node_linkage, Linkage);
+ffi_node_setter!(
+ facts_builder_set_node_call_type,
+ set_node_call_type,
+ CallType
+);
+ffi_node_string_setter!(facts_builder_set_node_source_file, set_node_source_file);
+ffi_node_string_setter!(facts_builder_set_node_function_type, set_node_function_type);
+ffi_node_setter!(
+ facts_builder_set_node_address_taken,
+ set_node_address_taken,
+ bool
+);
+
+#[unsafe(no_mangle)]
+pub extern "C" fn facts_builder_set_node_source_loc(
+ builder: *mut FactsBuilder,
+ module: ModuleHandle,
+ node: NodeID,
+ line: u32,
+ col: u32,
+) -> bool {
+ unsafe { builder.as_mut() }
+ .is_some_and(|builder| builder.set_node_source_loc(module, node, line, col))
+}
diff --git a/resolve-facts/rs/src/interner.rs b/resolve-facts/rs/src/interner.rs
new file mode 100644
index 000000000..44ca9b2d0
--- /dev/null
+++ b/resolve-facts/rs/src/interner.rs
@@ -0,0 +1,32 @@
+use std::collections::*;
+
+use crate::schema::*;
+
+#[derive(Default)]
+pub struct Interner {
+ ids: HashMap,
+ bytes: Vec,
+}
+
+impl Interner {
+ pub fn intern(&mut self, value: &str) -> Interned {
+ if let Some(&id) = self.ids.get(value) {
+ return id;
+ }
+
+ let offset = u32::try_from(self.bytes.len()).expect("intern pool exceeds 4 GiB");
+ let bytes = value.as_bytes();
+ let len = u32::try_from(bytes.len()).expect("interned string exceeds 4 GiB");
+
+ self.bytes.extend_from_slice(&len.to_le_bytes());
+ self.bytes.extend_from_slice(bytes);
+
+ let id = Interned(offset);
+ self.ids.insert(value.to_owned(), id);
+ id
+ }
+
+ pub fn bytes(&self) -> &[u8] {
+ &self.bytes
+ }
+}
diff --git a/resolve-facts/rs/src/lib.rs b/resolve-facts/rs/src/lib.rs
new file mode 100644
index 000000000..66c734799
--- /dev/null
+++ b/resolve-facts/rs/src/lib.rs
@@ -0,0 +1,12 @@
+#[cfg(not(target_endian = "little"))]
+compile_error!("the facts format currently requires a little-endian target");
+
+mod builder;
+mod interner;
+mod schema;
+mod utils;
+mod writer;
+
+pub use builder::{FactsBuilder, ModuleHandle};
+pub use schema::*;
+pub use writer::FactsBuf;
diff --git a/resolve-facts/rs/src/schema.rs b/resolve-facts/rs/src/schema.rs
new file mode 100644
index 000000000..2b565fbc2
--- /dev/null
+++ b/resolve-facts/rs/src/schema.rs
@@ -0,0 +1,541 @@
+// notes:
+// - some fields are over-sized to pad the struct to alignment
+
+use std::mem::*;
+
+use bytemuck::*;
+
+pub const FORMAT_VERSION: u32 = 1;
+
+macro_rules! enum_from_u8 {
+ ($ty:ty, $($value:path),+ $(,)?) => {
+ impl TryFrom for $ty {
+ type Error = u8;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ $(value if value == $value as u8 => Ok($value),)+
+ value => Err(value),
+ }
+ }
+ }
+ };
+}
+
+// Node identifier: index in its parent modules Node array
+pub type NodeID = u32;
+
+// Offset into modules interned string pool
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Pod, Zeroable)]
+pub struct Interned(pub u32);
+
+#[allow(dead_code)] // cbindgen
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum NodeType {
+ Module = 0,
+ Function = 1,
+ Argument = 2,
+ BasicBlock = 3,
+ Instruction = 4,
+ GlobalVariable = 5,
+}
+
+#[allow(dead_code)] // cbindgen
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Linkage {
+ Other = 0,
+ ExternalLinkage = 1,
+}
+
+#[allow(dead_code)] // cbindgen
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum CallType {
+ Direct = 0,
+ Indirect = 1,
+}
+
+enum_from_u8!(
+ NodeType,
+ NodeType::Module,
+ NodeType::Function,
+ NodeType::Argument,
+ NodeType::BasicBlock,
+ NodeType::Instruction,
+ NodeType::GlobalVariable,
+);
+enum_from_u8!(Linkage, Linkage::Other, Linkage::ExternalLinkage);
+enum_from_u8!(CallType, CallType::Direct, CallType::Indirect);
+
+// positions in Node.meta indicating which properties are present.
+pub const P_IDX: u32 = 1 << 0;
+pub const P_NAME: u32 = 1 << 1;
+pub const P_OPCODE: u32 = 1 << 2;
+pub const P_LINKAGE: u32 = 1 << 3;
+pub const P_CALL_TYPE: u32 = 1 << 4;
+pub const P_SOURCE_LOC: u32 = 1 << 5;
+pub const P_SOURCE_FILE: u32 = 1 << 6;
+pub const P_FUNCTION_TYPE: u32 = 1 << 7;
+pub const P_ADDRESS_TAKEN: u32 = 1 << 8;
+
+#[allow(dead_code)] // cbindgen
+pub const PRESENT_MASK: u32 = u16::MAX as u32;
+pub const NODE_TYPE_SHIFT: u32 = 16;
+#[allow(dead_code)] // cbindgen
+pub const NODE_TYPE_MASK: u32 = 0xff << NODE_TYPE_SHIFT;
+pub const LINKAGE_SHIFT: u32 = 24;
+pub const LINKAGE_MASK: u32 = 0x0f << LINKAGE_SHIFT;
+pub const CALL_TYPE_SHIFT: u32 = 28;
+pub const CALL_TYPE_MASK: u32 = 0x0f << CALL_TYPE_SHIFT;
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)]
+pub struct Node {
+ pub meta: u32,
+ pub idx: u32,
+ pub name: Interned,
+ pub opcode: Interned,
+ pub source_line: u32,
+ pub source_col: u32,
+ pub source_file: Interned,
+ pub function_type: Interned,
+}
+
+#[allow(dead_code)] // reader
+impl Node {
+ pub const fn new(ty: NodeType) -> Self {
+ Self {
+ meta: (ty as u32) << NODE_TYPE_SHIFT,
+ idx: 0,
+ name: Interned(0),
+ opcode: Interned(0),
+ source_line: 0,
+ source_col: 0,
+ source_file: Interned(0),
+ function_type: Interned(0),
+ }
+ }
+
+ pub const fn present(&self) -> u16 {
+ (self.meta & PRESENT_MASK) as u16
+ }
+
+ pub const fn has(&self, property: u32) -> bool {
+ self.meta & property != 0
+ }
+
+ pub const fn node_type_raw(&self) -> u8 {
+ ((self.meta & NODE_TYPE_MASK) >> NODE_TYPE_SHIFT) as u8
+ }
+
+ pub const fn linkage_raw(&self) -> Option {
+ if self.has(P_LINKAGE) {
+ Some(((self.meta & LINKAGE_MASK) >> LINKAGE_SHIFT) as u8)
+ } else {
+ None
+ }
+ }
+
+ pub const fn call_type_raw(&self) -> Option {
+ if self.has(P_CALL_TYPE) {
+ Some(((self.meta & CALL_TYPE_MASK) >> CALL_TYPE_SHIFT) as u8)
+ } else {
+ None
+ }
+ }
+
+ pub fn node_type(&self) -> Result {
+ self.node_type_raw().try_into()
+ }
+
+ pub fn linkage(&self) -> Result