use sim_kernel::{
AbiVersion, Cx, Dependency, Export, Expr, Lib, LibId, LibManifest, LibTarget, Linker, LoadCx,
Result, Symbol, Value, Version,
};
pub enum SurfaceField {
Symbol(Symbol),
Str(String),
Symbols(Vec<Symbol>),
Strs(Vec<String>),
Bool(bool),
U64(u64),
Expr(Expr),
}
pub struct SurfaceValueSpec {
pub symbol: Symbol,
pub fields: Vec<(Symbol, SurfaceField)>,
}
pub struct SurfacePackSpec {
pub lib_id: Symbol,
pub values: Vec<SurfaceValueSpec>,
}
pub struct SurfacePackLib {
pub spec: SurfacePackSpec,
}
impl Lib for SurfacePackLib {
fn manifest(&self) -> LibManifest {
LibManifest {
id: self.spec.lib_id.clone(),
version: Version(env!("CARGO_PKG_VERSION").to_owned()),
abi: AbiVersion { major: 0, minor: 1 },
target: LibTarget::HostRegistered,
requires: Vec::<Dependency>::new(),
capabilities: Vec::new(),
exports: self
.spec
.values
.iter()
.map(|value| Export::Value {
symbol: value.symbol.clone(),
})
.collect(),
}
}
fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
for value in &self.spec.values {
let card = build_card(cx, &value.fields)?;
linker.value(value.symbol.clone(), card)?;
}
Ok(())
}
}
fn build_card(cx: &mut LoadCx, fields: &[(Symbol, SurfaceField)]) -> Result<Value> {
let mut entries = Vec::with_capacity(fields.len());
for (key, field) in fields {
let value = match field {
SurfaceField::Symbol(symbol) => cx.factory().symbol(symbol.clone())?,
SurfaceField::Str(text) => cx.factory().string(text.clone())?,
SurfaceField::Bool(flag) => cx.factory().bool(*flag)?,
SurfaceField::Symbols(symbols) => {
let items = symbols
.iter()
.map(|symbol| cx.factory().symbol(symbol.clone()))
.collect::<Result<Vec<_>>>()?;
cx.factory().list(items)?
}
SurfaceField::Strs(texts) => {
let items = texts
.iter()
.map(|text| cx.factory().string(text.clone()))
.collect::<Result<Vec<_>>>()?;
cx.factory().list(items)?
}
other => cx.factory().expr(field_to_expr(other))?,
};
entries.push((key.clone(), value));
}
cx.factory().table(entries)
}
pub fn card_expr(spec: &SurfaceValueSpec) -> Expr {
Expr::Map(
spec.fields
.iter()
.map(|(key, field)| (Expr::Symbol(key.clone()), field_to_expr(field)))
.collect(),
)
}
fn field_to_expr(field: &SurfaceField) -> Expr {
match field {
SurfaceField::Symbol(symbol) => Expr::Symbol(symbol.clone()),
SurfaceField::Str(text) => Expr::String(text.clone()),
SurfaceField::Bool(flag) => Expr::Bool(*flag),
SurfaceField::U64(number) => sim_value::build::uint(*number),
SurfaceField::Symbols(symbols) => Expr::List(
symbols
.iter()
.map(|symbol| Expr::Symbol(symbol.clone()))
.collect(),
),
SurfaceField::Strs(texts) => Expr::List(
texts
.iter()
.map(|text| Expr::String(text.clone()))
.collect(),
),
SurfaceField::Expr(expr) => expr.clone(),
}
}
pub fn install_once(cx: &mut Cx, lib: &impl Lib) -> Result<bool> {
install_once_id(cx, lib).map(|id| id.is_some())
}
pub fn install_once_id(cx: &mut Cx, lib: &impl Lib) -> Result<Option<LibId>> {
let id = lib.manifest().id.clone();
if cx.registry().lib(&id).is_some() {
return Ok(None);
}
cx.load_lib(lib).map(Some)
}
pub fn installed_lib_id(cx: &Cx, lib: &impl Lib) -> Option<LibId> {
let id = lib.manifest().id;
cx.registry().lib(&id).map(|loaded| loaded.id)
}
#[cfg(test)]
mod tests {
use super::*;
fn pack() -> SurfacePackLib {
SurfacePackLib {
spec: SurfacePackSpec {
lib_id: Symbol::new("demo-pack"),
values: vec![
SurfaceValueSpec {
symbol: Symbol::qualified("demo", "Alpha"),
fields: vec![
(
Symbol::new("symbol"),
SurfaceField::Symbol(Symbol::qualified("demo", "Alpha")),
),
(Symbol::new("layer"), SurfaceField::Str("demo".to_owned())),
(Symbol::new("lossless"), SurfaceField::Bool(true)),
(Symbol::new("rank"), SurfaceField::U64(3)),
(
Symbol::new("tags"),
SurfaceField::Strs(vec!["a".to_owned(), "b".to_owned()]),
),
],
},
SurfaceValueSpec {
symbol: Symbol::qualified("demo", "Beta"),
fields: vec![(
Symbol::new("symbol"),
SurfaceField::Symbol(Symbol::qualified("demo", "Beta")),
)],
},
],
},
}
}
#[test]
fn manifest_exports_one_value_per_card() {
let manifest = pack().manifest();
assert_eq!(manifest.id, Symbol::new("demo-pack"));
let symbols: Vec<String> = manifest
.exports
.iter()
.filter_map(|export| match export {
Export::Value { symbol } => Some(symbol.to_string()),
_ => None,
})
.collect();
assert_eq!(
symbols,
vec!["demo/Alpha".to_owned(), "demo/Beta".to_owned()]
);
}
#[test]
fn card_expr_renders_every_field_kind() {
let spec = &pack().spec.values[0];
let Expr::Map(entries) = card_expr(spec) else {
panic!("card_expr must build a map");
};
assert_eq!(entries.len(), 5);
assert_eq!(
entries[0],
(
Expr::Symbol(Symbol::new("symbol")),
Expr::Symbol(Symbol::qualified("demo", "Alpha"))
)
);
assert_eq!(
entries[1],
(
Expr::Symbol(Symbol::new("layer")),
Expr::String("demo".to_owned())
)
);
assert_eq!(
entries[2],
(Expr::Symbol(Symbol::new("lossless")), Expr::Bool(true))
);
assert_eq!(
entries[3],
(Expr::Symbol(Symbol::new("rank")), sim_value::build::uint(3))
);
assert_eq!(
entries[4].1,
Expr::List(vec![
Expr::String("a".to_owned()),
Expr::String("b".to_owned())
])
);
}
#[test]
fn install_once_is_idempotent_and_loads_every_field_kind() {
let mut cx = sim_test_support::core_cx();
assert!(
install_once(&mut cx, &pack()).unwrap(),
"first install loads"
);
assert!(
!install_once(&mut cx, &pack()).unwrap(),
"second install is a no-op"
);
}
}