read what the binary says about itself: names, signatures, prototypes; gen v2
This commit is contained in:
parent
54ef572202
commit
c458b4cb50
34 changed files with 58363 additions and 192 deletions
417
src/schema.rs
417
src/schema.rs
|
|
@ -18,7 +18,7 @@ use crate::elf::CodeImage;
|
|||
use crate::profile::GameProfile;
|
||||
use crate::{live, model};
|
||||
use anyhow::Result;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
/// Byte offsets of the SchemaSystem reflection structs (SchemaClassInfoData_t / SchemaClassFieldData_t /
|
||||
|
|
@ -140,7 +140,6 @@ fn is_type_name(s: &str) -> bool {
|
|||
/// inventory. Sorted by class name.
|
||||
pub fn enumerate_schema(img: &CodeImage) -> Vec<SchemaClass> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for (slot, val) in img.reloc_slots() {
|
||||
if slot < 8 {
|
||||
continue;
|
||||
|
|
@ -152,10 +151,8 @@ pub fn enumerate_schema(img: &CodeImage) -> Vec<SchemaClass> {
|
|||
if !is_type_name(&name) {
|
||||
continue;
|
||||
}
|
||||
// No de-dup guard — `reloc_slots` iterates a slot-keyed map, so `slot - 8` is already unique.
|
||||
let base = slot - 8;
|
||||
if !seen.insert(base) {
|
||||
continue;
|
||||
}
|
||||
if let Some(cls) = parse_class(img, base, &name, val) {
|
||||
out.push(cls);
|
||||
}
|
||||
|
|
@ -164,6 +161,92 @@ pub fn enumerate_schema(img: &CodeImage) -> Vec<SchemaClass> {
|
|||
out
|
||||
}
|
||||
|
||||
/// One registered Source-2 enum recovered from the schema tables — the semantic vocabulary
|
||||
/// (`MoveType_t`, `gear_slot_t`, `DamageTypes_t`) that a raw field offset and an integer width cannot
|
||||
/// supply on their own.
|
||||
pub struct SchemaEnum {
|
||||
pub name: String,
|
||||
/// Underlying integer width in bytes — the binding records it, so a byte-sized enum
|
||||
/// (`MoveType_t`) is distinguishable from a word-sized one (`gear_slot_t`) without inference.
|
||||
pub size: u8,
|
||||
pub align: u8,
|
||||
/// Enumerators in DECLARATION order (names are unique; values are not — aliases like
|
||||
/// `MOVETYPE_LAST` / `MOVETYPE_INVALID` legitimately share one).
|
||||
pub values: Vec<(String, i64)>,
|
||||
}
|
||||
|
||||
// A `CSchemaEnumBinding`, relative to the slot holding its name pointer.
|
||||
const EB_TYPE_NAME: u64 = 0; // char* — the enum's type name (the reloc slot this is found by)
|
||||
const EB_WIDTH: u64 = 16; // u8 size, u8 alignment, u16 flags, u32 enumerator count
|
||||
const EB_VALUES: u64 = 24; // -> the enumerator array
|
||||
const EV_STRIDE: u64 = 32; // one enumerator: char* name, i64 value, then metadata
|
||||
const EV_VALUE: u64 = 8;
|
||||
/// Enumerator-count sanity bound. The largest real CS2 enum is ~100 values; this only has to reject a
|
||||
/// field that isn't a count at all before it drives an allocation.
|
||||
const EB_MAX_VALUES: u32 = 4096;
|
||||
|
||||
/// Enumerate every registered enum in `img`, alongside [`enumerate_schema`]'s classes. Same reloc-driven
|
||||
/// discovery: an enum binding is found by the slot holding its type-name pointer, then accepted only if
|
||||
/// the width/count word and the enumerator array both read as what they claim to be — so a layout change
|
||||
/// yields fewer enums, never wrong ones. Sorted by name.
|
||||
pub fn enumerate_enums(img: &CodeImage) -> Vec<SchemaEnum> {
|
||||
let mut out = Vec::new();
|
||||
for (slot, val) in img.reloc_slots() {
|
||||
let Some(name) = img.read_c_string(val) else {
|
||||
continue;
|
||||
};
|
||||
// `reloc_slots` iterates a slot-keyed map, so a per-slot de-dup set can reject nothing; the real
|
||||
// de-dup is by NAME, at the sort/dedup below (a shared enum is registered by several libraries).
|
||||
if !is_type_name(&name) {
|
||||
continue;
|
||||
}
|
||||
let base = slot.wrapping_sub(EB_TYPE_NAME);
|
||||
let Some(w) = img.read_ptr(base.wrapping_add(EB_WIDTH)) else {
|
||||
continue;
|
||||
};
|
||||
let (size, align, count) = (w as u8, (w >> 8) as u8, (w >> 32) as u32);
|
||||
if !matches!(size, 1 | 2 | 4 | 8)
|
||||
|| !matches!(align, 1 | 2 | 4 | 8)
|
||||
|| count == 0
|
||||
|| count > EB_MAX_VALUES
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(arr) = img
|
||||
.read_ptr(base.wrapping_add(EB_VALUES))
|
||||
.filter(|&a| a != 0)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// Every enumerator must read cleanly; a partial read means this was not an enum binding.
|
||||
let mut values = Vec::with_capacity(count as usize);
|
||||
for i in 0..u64::from(count) {
|
||||
let rec = arr.wrapping_add(i.wrapping_mul(EV_STRIDE));
|
||||
let (Some(n), Some(v)) = (
|
||||
img.read_ptr(rec).and_then(|p| img.read_c_string(p)),
|
||||
img.read_i64(rec.wrapping_add(EV_VALUE)),
|
||||
) else {
|
||||
break;
|
||||
};
|
||||
if n.is_empty() {
|
||||
break;
|
||||
}
|
||||
values.push((n, v));
|
||||
}
|
||||
if values.len() == count as usize {
|
||||
out.push(SchemaEnum {
|
||||
name,
|
||||
size,
|
||||
align,
|
||||
values,
|
||||
});
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
out.dedup_by(|a, b| a.name == b.name); // one binding per name; libs re-register shared enums
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_class(img: &CodeImage, base: u64, name: &str, name_ptr: u64) -> Option<SchemaClass> {
|
||||
let size = img.read_i32(base.wrapping_add(CI_SIZE))?;
|
||||
if size <= 0 || size >= (1 << 23) {
|
||||
|
|
@ -274,6 +357,12 @@ pub(crate) fn live_schema(
|
|||
let mut classes: BTreeMap<String, BTreeMap<String, Field>> = BTreeMap::new();
|
||||
let (mut typed, mut untyped) = (0usize, 0usize);
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut enums: BTreeMap<String, model::EnumDef> = BTreeMap::new();
|
||||
// The schema states each registered class's instance size — the exact half of the layout picture.
|
||||
let mut registered_sizes: BTreeMap<String, usize> = BTreeMap::new();
|
||||
// The base graph the schema already recovers — exported, so a consumer can resolve an inherited
|
||||
// field, and consulted here so the SysV verdict sees inherited members.
|
||||
let mut bases: BTreeMap<String, Vec<model::BaseClass>> = BTreeMap::new();
|
||||
let mut nlibs = 0usize;
|
||||
for &lib in prof.libs {
|
||||
let Ok(img) = crate::locate::load_lib(dir, lib) else {
|
||||
|
|
@ -281,6 +370,18 @@ pub(crate) fn live_schema(
|
|||
};
|
||||
let Some(base) = live.base(lib) else { continue }; // lib not mapped in the process -> skip
|
||||
nlibs += 1;
|
||||
// Enum bindings are static, so they come from the IMAGE — no process read, unlike field types.
|
||||
// First library wins, matching the class precedence: a shared enum has one definition.
|
||||
for e in enumerate_enums(&img) {
|
||||
enums.entry(e.name).or_insert_with(|| model::EnumDef {
|
||||
size: e.size,
|
||||
values: e
|
||||
.values
|
||||
.into_iter()
|
||||
.map(|(name, value)| model::EnumValue { name, value })
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
for c in &enumerate_schema(&img) {
|
||||
// a shared class already taken from an earlier (higher-precedence) lib — identical layout, skip
|
||||
if !seen.insert(c.name.clone()) {
|
||||
|
|
@ -334,12 +435,54 @@ pub(crate) fn live_schema(
|
|||
},
|
||||
);
|
||||
}
|
||||
if let Ok(sz) = usize::try_from(c.size) {
|
||||
registered_sizes.insert(c.name.clone(), sz);
|
||||
}
|
||||
if !c.bases.is_empty() {
|
||||
bases.insert(
|
||||
c.name.clone(),
|
||||
c.bases
|
||||
.iter()
|
||||
.map(|b| model::BaseClass {
|
||||
name: b.name.clone(),
|
||||
offset: b.offset,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
classes.insert(c.name.clone(), fmap);
|
||||
}
|
||||
}
|
||||
let (types, cal) = derive_type_layouts(&classes, ®istered_sizes, &bases);
|
||||
// A size is only useful to a caller once it reaches the FIELD: `size` was zero for every aggregate,
|
||||
// which reads as "unknown" and is exactly what the layout pass now answers.
|
||||
for fields in classes.values_mut() {
|
||||
for f in fields.values_mut() {
|
||||
if f.size == 0
|
||||
&& let Some(t) = base_type(&f.ty).and_then(|b| types.get(b))
|
||||
{
|
||||
// The field's EXTENT, so a consumer can bound a read: a fixed array spans
|
||||
// element x count. Writing the element size here would understate `char[128]` as 1.
|
||||
f.size = t.size.saturating_mul(array_len(&f.ty));
|
||||
}
|
||||
}
|
||||
}
|
||||
let derived_sizes = classes
|
||||
.values()
|
||||
.flat_map(|c| c.values())
|
||||
.filter(|f| f.size > 0)
|
||||
.count();
|
||||
eprintln!(
|
||||
"typed netvars: {} classes across {nlibs} libs, {typed} typed fields, {untyped} unresolved",
|
||||
classes.len()
|
||||
"typed netvars: {} classes across {nlibs} libs, {typed} typed fields, {untyped} unresolved; \
|
||||
{} enums / {} enumerators; {} type layouts ({derived_sizes}/{} fields sized, \
|
||||
field-gap calibration {}/{} exact)",
|
||||
classes.len(),
|
||||
enums.len(),
|
||||
enums.values().map(|e| e.values.len()).sum::<usize>(),
|
||||
types.len(),
|
||||
typed + untyped,
|
||||
cal.exact,
|
||||
cal.checked
|
||||
);
|
||||
Ok(Schema {
|
||||
meta: SchemaMeta {
|
||||
|
|
@ -347,7 +490,267 @@ pub(crate) fn live_schema(
|
|||
source_build: source_build.to_string(),
|
||||
typed,
|
||||
untyped,
|
||||
enums: enums.len(),
|
||||
types: types.len(),
|
||||
},
|
||||
classes,
|
||||
bases,
|
||||
enums,
|
||||
types,
|
||||
})
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// Type layouts — what a caller needs to PASS a value, which an offset alone cannot supply
|
||||
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// SysV classification for the engine value types the SchemaSystem does NOT register, and whose size
|
||||
/// alone cannot settle how they travel.
|
||||
///
|
||||
/// At 16 bytes or less an aggregate goes in SSE registers when every member is floating-point and in
|
||||
/// integer registers otherwise, and no derived size distinguishes those two. Above 16 bytes the size
|
||||
/// settles it, so nothing needs declaring. This is therefore the ONE place the deriver declares rather
|
||||
/// than derives, deliberately kept to a closed set of engine primitives — each entry is what the type
|
||||
/// demonstrably IS, not a guess: the math types are plain float aggregates, and everything else here is
|
||||
/// a pointer, a handle or a packed integer.
|
||||
const UNREGISTERED_CLASSES: &[(&str, model::SysvClass)] = &[
|
||||
// All-float aggregates — SSE. `Vector` by value costs TWO SSE registers; by reference, one integer.
|
||||
("Vector", model::SysvClass::Sse),
|
||||
("VectorWS", model::SysvClass::Sse),
|
||||
("Vector2D", model::SysvClass::Sse),
|
||||
("Vector4D", model::SysvClass::Sse),
|
||||
("QAngle", model::SysvClass::Sse),
|
||||
("Quaternion", model::SysvClass::Sse),
|
||||
("RadianEuler", model::SysvClass::Sse),
|
||||
("QuaternionStorage", model::SysvClass::Sse),
|
||||
// Pointers, handles and packed integers — INTEGER.
|
||||
("CUtlString", model::SysvClass::Integer),
|
||||
("CUtlSymbolLarge", model::SysvClass::Integer),
|
||||
("CUtlSymbol", model::SysvClass::Integer),
|
||||
("CGlobalSymbol", model::SysvClass::Integer),
|
||||
("CUtlStringToken", model::SysvClass::Integer),
|
||||
("CHandle", model::SysvClass::Integer),
|
||||
("CEntityHandle", model::SysvClass::Integer),
|
||||
("CStrongHandle", model::SysvClass::Integer),
|
||||
("CWeakHandle", model::SysvClass::Integer),
|
||||
("CGameSoundEventName", model::SysvClass::Integer),
|
||||
("Color", model::SysvClass::Integer),
|
||||
("CTransform", model::SysvClass::Memory), // 32 bytes; stated for clarity, size settles it anyway
|
||||
];
|
||||
|
||||
/// The SysV boundary: an aggregate above this is passed in memory, so its size settles its class.
|
||||
const SYSV_REGISTER_LIMIT: usize = 16;
|
||||
/// A field-gap size is accepted only with this much agreement across observations — the modal gap has to
|
||||
/// dominate, or the "next field" is padding/union noise rather than this field's extent.
|
||||
const GAP_AGREEMENT: f64 = 0.8;
|
||||
/// …and only with at least this many observations, so one lucky class cannot mint a size.
|
||||
const GAP_MIN_OBS: usize = 4;
|
||||
|
||||
/// A builtin's SysV class. The floating types travel in SSE registers, every other builtin in integer
|
||||
/// ones — a property of the ABI, not of Valve's code, which is why it is stated here rather than derived.
|
||||
fn builtin_sysv(t: &str) -> Option<model::SysvClass> {
|
||||
match t {
|
||||
"float32" | "float64" | "double" => Some(model::SysvClass::Sse),
|
||||
"int8" | "uint8" | "char" | "bool" | "int16" | "uint16" | "int32" | "uint32" | "int64"
|
||||
| "uint64" => Some(model::SysvClass::Integer),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How well the field-gap inference reproduced the sizes that are known exactly — the same free-oracle
|
||||
/// idea as the entity-IO ABI check: the builtins have an independently known size, so running the
|
||||
/// inference over them and comparing is a per-build test of the inference itself, not an assumption.
|
||||
pub struct GapCalibration {
|
||||
pub checked: usize,
|
||||
pub exact: usize,
|
||||
}
|
||||
|
||||
/// The bare type name behind a field's declared type: array suffix stripped, template arguments dropped.
|
||||
/// `None` for a pointer (its size is the pointer's, and it says nothing about the pointee) or a bitfield.
|
||||
fn base_type(ty: &str) -> Option<&str> {
|
||||
let t = ty.trim().split('[').next()?.trim();
|
||||
if t.ends_with('*') || t.starts_with("bitfield") || t.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(t.split('<').next()?.trim())
|
||||
}
|
||||
|
||||
/// Every type NAME a declared field type mentions: the outer type plus each template argument, since an
|
||||
/// inner type is a real type a consumer must know — `CUtlLeanVector<CPulseRuntimeMethodArg>` is how the
|
||||
/// element type of a Pulse method's argument list is spelled, and stripping the template arguments loses it.
|
||||
fn mentioned_types(ty: &str) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(b) = base_type(ty) {
|
||||
out.push(b);
|
||||
}
|
||||
// Template arguments, comma-split at depth 1 so a nested template stays with its parent.
|
||||
if let Some(open) = ty.find('<') {
|
||||
let inner = &ty[open + 1..ty.rfind('>').unwrap_or(ty.len())];
|
||||
let (mut depth, mut start) = (0usize, 0usize);
|
||||
for (i, c) in inner.char_indices() {
|
||||
match c {
|
||||
'<' | '(' | '[' => depth += 1,
|
||||
'>' | ')' | ']' => depth = depth.saturating_sub(1),
|
||||
',' if depth == 0 => {
|
||||
out.extend(mentioned_types(&inner[start..i]));
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.extend(mentioned_types(&inner[start..]));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The declared array length of a field type (`float32[3]` -> 3), else 1.
|
||||
fn array_len(ty: &str) -> usize {
|
||||
ty.rsplit_once('[')
|
||||
.and_then(|(_, n)| n.strip_suffix(']'))
|
||||
.and_then(|n| n.trim().parse::<usize>().ok())
|
||||
.filter(|&n| n > 0)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Is every member of `ty`, inherited members included, a floating-point value? `None` when the answer
|
||||
/// cannot be established — an unknown base, or a member whose own type is not resolvable — because
|
||||
/// "unknown" and "not all float" are different answers and only one of them is safe to act on.
|
||||
fn all_float(
|
||||
ty: &str,
|
||||
classes: &BTreeMap<String, BTreeMap<String, model::Field>>,
|
||||
bases: &BTreeMap<String, Vec<model::BaseClass>>,
|
||||
depth: usize,
|
||||
) -> Option<bool> {
|
||||
if depth > 8 {
|
||||
return None; // pathological or cyclic hierarchy — decline rather than guess
|
||||
}
|
||||
let fields = classes.get(ty)?;
|
||||
for b in bases.get(ty).map(Vec::as_slice).unwrap_or_default() {
|
||||
if !all_float(&b.name, classes, bases, depth + 1)? {
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
// A class with no members of its own and no bases tells us nothing about how it travels.
|
||||
if fields.is_empty() && bases.get(ty).is_none_or(Vec::is_empty) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
fields
|
||||
.values()
|
||||
.all(|f| matches!(base_type(&f.ty), Some("float32" | "float64"))),
|
||||
)
|
||||
}
|
||||
|
||||
/// Recover a size and a SysV class for every type the schema's fields refer to.
|
||||
///
|
||||
/// Two independent routes, and which one produced a given answer is recorded rather than blurred:
|
||||
/// a REGISTERED class states its own instance size, and everything else is inferred from the distance to
|
||||
/// the next field — schema fields are laid out in offset order, so that gap IS the field's extent. The
|
||||
/// inference is calibrated on the types whose size is independently known: every primitive
|
||||
/// (`float32`, `int64`, …) comes back exact.
|
||||
pub fn derive_type_layouts(
|
||||
classes: &BTreeMap<String, BTreeMap<String, model::Field>>,
|
||||
registered_sizes: &BTreeMap<String, usize>,
|
||||
bases: &BTreeMap<String, Vec<model::BaseClass>>,
|
||||
) -> (BTreeMap<String, model::TypeLayout>, GapCalibration) {
|
||||
// base type -> observed per-element gap -> how many times it was seen
|
||||
let mut gaps: BTreeMap<&str, BTreeMap<usize, usize>> = BTreeMap::new();
|
||||
for fields in classes.values() {
|
||||
let mut by_off: Vec<(&model::Field, &str)> =
|
||||
fields.values().map(|f| (f, f.ty.as_str())).collect();
|
||||
by_off.sort_by_key(|(f, _)| f.offset);
|
||||
for w in by_off.windows(2) {
|
||||
let (f, ty) = w[0];
|
||||
let gap = w[1].0.offset - f.offset;
|
||||
// A non-positive gap is a union or an overlapping bitfield, not an extent.
|
||||
let (Some(base), true) = (base_type(ty), gap > 0) else {
|
||||
continue;
|
||||
};
|
||||
let n = array_len(ty);
|
||||
if gap as usize % n != 0 {
|
||||
continue; // the gap does not divide into the declared element count — not this field's
|
||||
}
|
||||
*gaps
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.entry(gap as usize / n)
|
||||
.or_default() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let declared: BTreeMap<&str, model::SysvClass> = UNREGISTERED_CLASSES.iter().copied().collect();
|
||||
let mut out = BTreeMap::new();
|
||||
// Every type any field refers to — a type used only behind a pointer still deserves an entry when
|
||||
// its size is known from the schema.
|
||||
let mut wanted: BTreeSet<&str> = BTreeSet::new();
|
||||
for fields in classes.values() {
|
||||
for f in fields.values() {
|
||||
wanted.extend(mentioned_types(&f.ty));
|
||||
}
|
||||
}
|
||||
wanted.extend(gaps.keys().copied());
|
||||
// Every registered class, whether or not any field happens to name it — the schema states its size, so
|
||||
// withholding the entry would be losing an answer we already hold.
|
||||
wanted.extend(registered_sizes.keys().map(String::as_str));
|
||||
|
||||
let mut cal = GapCalibration {
|
||||
checked: 0,
|
||||
exact: 0,
|
||||
};
|
||||
for ty in wanted {
|
||||
let builtin = builtin_size(ty);
|
||||
// Where a size is known exactly, CHECK the inference against it rather than using the inference.
|
||||
if builtin > 0
|
||||
&& let Some(hist) = gaps.get(ty)
|
||||
&& let Some((&sz, _)) = hist.iter().max_by_key(|&(_, n)| *n)
|
||||
{
|
||||
cal.checked += 1;
|
||||
cal.exact += usize::from(sz == builtin as usize);
|
||||
}
|
||||
let (size, source, obs, agree) = match (builtin, registered_sizes.get(ty)) {
|
||||
// A builtin's size is fixed by the ABI.
|
||||
(b, _) if b > 0 => (b as usize, model::LayoutSource::Declared, None, None),
|
||||
// The schema states a registered class's size — no inference needed.
|
||||
(_, Some(&sz)) => (sz, model::LayoutSource::Schema, None, None),
|
||||
_ => {
|
||||
let Some(hist) = gaps.get(ty) else { continue };
|
||||
let total: usize = hist.values().sum();
|
||||
let (&sz, &n) = hist.iter().max_by_key(|&(_, n)| *n).expect("non-empty");
|
||||
if total < GAP_MIN_OBS || (n as f64) < GAP_AGREEMENT * total as f64 {
|
||||
continue; // too thin or too contested to state a size
|
||||
}
|
||||
(sz, model::LayoutSource::FieldGap, Some(total), Some(n))
|
||||
}
|
||||
};
|
||||
// Above the register limit the size decides. At or below it, the question is whether every member
|
||||
// is floating-point — derivable for a registered class by inspecting its fields, and declared for
|
||||
// the closed set of primitives the schema omits.
|
||||
let sysv = if let Some(c) = builtin_sysv(ty) {
|
||||
c
|
||||
} else if size > SYSV_REGISTER_LIMIT {
|
||||
model::SysvClass::Memory
|
||||
} else if let Some(&c) = declared.get(ty) {
|
||||
c
|
||||
} else {
|
||||
// SSE requires that EVERY member is floating-point — including inherited ones. Judging on
|
||||
// a class's own fields alone calls a type SSE whose base contributes the first eightbyte
|
||||
// (a pointer), which is the difference between passing it in XMM0 and in RDI.
|
||||
match all_float(ty, classes, bases, 0) {
|
||||
Some(true) => model::SysvClass::Sse,
|
||||
Some(false) => model::SysvClass::Integer,
|
||||
None => model::SysvClass::Unknown,
|
||||
}
|
||||
};
|
||||
out.insert(
|
||||
ty.to_string(),
|
||||
model::TypeLayout {
|
||||
size,
|
||||
sysv,
|
||||
source,
|
||||
observations: obs,
|
||||
agreement: agree,
|
||||
},
|
||||
);
|
||||
}
|
||||
(out, cal)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue