#![cfg(feature = "wasm")]
use crate::admission::RejectionCode;
use crate::catalog::Catalog;
use crate::kernel::Kernel;
use crate::types::{
Atom8, EpochId, FactBlock8, FactRow8, PredicateId, ProofMode, QueryAtom8, SourceId, TermId,
};
use serde_json::json;
use wasm_bindgen::prelude::*;
const MAX_INPUT_LEN: usize = 10 * 1024 * 1024;
const MAX_ANSWERS: usize = 128;
fn js_err(message: &str) -> JsValue {
let json = serde_json::to_string(&json!({"error": message}))
.unwrap_or_else(|_| format!(r#"{{"error":"{}"}}"#, message));
JsValue::from_str(&json)
}
fn to_js_str<T: serde::Serialize>(v: &T) -> Result<JsValue, JsValue> {
serde_json::to_string(v)
.map(|s| JsValue::from_str(&s))
.map_err(|e| js_err(&format!("serialize: {e}")))
}
fn rejection_message(code: RejectionCode) -> &'static str {
match code {
RejectionCode::ArityCapExceeded => "arity exceeds cap of 8",
RejectionCode::RuleBodyCapExceeded => "rule body exceeds cap of 8 atoms",
RejectionCode::VariableCapExceeded => "rule declares more than 8 variables",
RejectionCode::ProofFanInExceeded => "proof node has more than 8 children",
RejectionCode::StateSurfaceExceeded => "rule-family state surface exceeds 256",
RejectionCode::StringQueryNotAdmitted => "string queries are not admitted (use interned IDs)",
RejectionCode::RuntimeParseRejected => "runtime parsing is not admitted",
RejectionCode::TextualMetaCallRejected => "textual meta-calls are not admitted",
RejectionCode::UninternedTerm => "bound argument refers to an uninterned term (TermId 0 is sentinel)",
RejectionCode::OperatorDeclarationRejected => "operator declarations are not admitted",
RejectionCode::UnstratifiedNegation => "negation is not stratified",
RejectionCode::UnboundedRecursion => "recursion must be bounded or declared",
RejectionCode::NonIndexableBuiltin => "built-in predicate is not indexable",
RejectionCode::DynamicMutationNotAdmitted => "assert/retract (dynamic mutation) is not admitted",
RejectionCode::CutNotAdmitted => "cut (!) is not admitted",
RejectionCode::ForeignContractMissing => "foreign predicate has no replay contract",
RejectionCode::NondeterministicForeignCall => "foreign predicate is non-deterministic",
RejectionCode::SideEffectInKernel => "side-effects inside the kernel boundary are not admitted",
RejectionCode::ReplayContractMissing => "replay contract is missing",
RejectionCode::PredicateNotInCatalog => "predicate id is not registered in the catalog",
RejectionCode::ArityMismatch => "atom arity does not match catalog metadata",
RejectionCode::BindingMaskOutOfRange => "binding_mask references bit positions beyond the atom arity",
RejectionCode::PaddingNotSentinel => "argument slots beyond arity must be sentinel (TermId 0)",
RejectionCode::BodyMaskMismatch => "body_mask must equal (1 << body_len) - 1",
RejectionCode::NegationMaskOutOfRange => "negation_mask references positions beyond body_len",
RejectionCode::BuiltinMaskOutOfRange => "builtin_mask references positions beyond body_len",
RejectionCode::ProofMaskOutOfRange => "proof_mask references positions beyond body_len",
RejectionCode::FeatureBitNotAdmitted => "feature_mask contains a reserved or unrecognised bit",
RejectionCode::NegationRequiresFeature => "negation_mask is set but FeatureBit::StratifiedNegation is not in feature_mask",
RejectionCode::BuiltinRequiresFeature => "builtin_mask is set but FeatureBit::Equality or TypedComparisons is not in feature_mask",
}
}
#[derive(serde::Deserialize)]
struct FactRowInput {
pred_id: u32,
arity: u8,
args: Vec<u32>,
#[serde(default)]
source_id: u32,
}
impl FactRowInput {
fn into_fact_row(self) -> FactRow8 {
let pid = PredicateId(self.pred_id);
let args: Vec<TermId> = self.args.iter().map(|&v| TermId(v)).collect();
FactRow8::new(pid, self.arity, &args, SourceId(self.source_id))
}
}
#[derive(serde::Deserialize)]
struct FactBlockInput {
pred_id: u32,
arity: u8,
#[serde(default)]
rows: Vec<FactRowInput>,
}
impl FactBlockInput {
fn into_fact_block(self) -> FactBlock8 {
let pid = PredicateId(self.pred_id);
let rows: Vec<FactRow8> = self.rows.into_iter().map(|r| r.into_fact_row()).collect();
FactBlock8::new(pid, self.arity, rows)
}
}
#[derive(serde::Deserialize)]
struct AtomInput {
pred_id: u32,
arity: u8,
#[serde(default)]
args: Vec<u32>,
#[serde(default)]
binding_mask: u8,
}
impl AtomInput {
fn into_atom(self) -> Atom8 {
let pid = PredicateId(self.pred_id);
let args: Vec<TermId> = self.args.iter().map(|&v| TermId(v)).collect();
let mut atom = Atom8::new(pid, self.arity, &args);
atom.binding_mask = self.binding_mask;
atom
}
}
#[derive(serde::Deserialize)]
struct QueryInput {
atom: AtomInput,
#[serde(default)]
binding_mask: u8,
#[serde(default)]
output_mask: u8,
#[serde(default = "default_proof_mode")]
proof_mode: ProofMode,
#[serde(default)]
epoch: u64,
}
fn default_proof_mode() -> ProofMode {
ProofMode::PositiveOnly
}
impl QueryInput {
fn into_query_atom(mut self) -> QueryAtom8 {
self.atom.binding_mask = self.binding_mask;
QueryAtom8 {
atom: self.atom.into_atom(),
output_mask: self.output_mask,
proof_mode: self.proof_mode,
epoch: EpochId(self.epoch),
}
}
}
#[wasm_bindgen]
pub fn prolog8_show() -> Result<JsValue, JsValue> {
to_js_str(&json!({
"engine": crate::ENGINE_NAME,
"version": crate::ENGINE_VERSION,
"caps": {
"arity": crate::ARITY_CAP,
"body": crate::BODY_CAP,
"vars": crate::VAR_CAP,
"binding_patterns": crate::BINDING_PATTERNS,
"max_answers": MAX_ANSWERS,
}
}))
}
#[wasm_bindgen]
pub fn prolog8_query(input_json: &str) -> Result<JsValue, JsValue> {
if input_json.len() > MAX_INPUT_LEN {
return Err(js_err(&format!(
"input exceeds {MAX_INPUT_LEN} bytes (got {})",
input_json.len()
)));
}
#[derive(serde::Deserialize)]
struct Input {
catalog: Catalog,
#[serde(default)]
facts: Vec<FactBlockInput>,
#[serde(default)]
rules: Vec<crate::types::Rule8>,
query: QueryInput,
}
let parsed: Input =
serde_json::from_str(input_json).map_err(|e| js_err(&format!("schema rejected: {e}")))?;
let mut kernel = Kernel::new(parsed.catalog);
for block in parsed.facts {
kernel.load_facts(block.into_fact_block()).map_err(|c| {
js_err(&format!(
"fact admission rejected: {}",
rejection_message(c)
))
})?;
}
for rule in parsed.rules {
kernel.load_rule(rule).map_err(|c| {
js_err(&format!(
"rule admission rejected: {}",
rejection_message(c)
))
})?;
}
let query = parsed.query.into_query_atom();
match kernel.query(&query) {
crate::kernel::QueryResult::Answered(mut answers) => {
if answers.len() > MAX_ANSWERS {
answers.truncate(MAX_ANSWERS);
to_js_str(&json!({ "TruncatedAnswers": answers }))
} else {
to_js_str(&json!({ "Answered": answers }))
}
}
other => to_js_str(&other),
}
}
#[wasm_bindgen]
pub fn prolog8_replay(input_json: &str) -> Result<JsValue, JsValue> {
if input_json.len() > MAX_INPUT_LEN {
return Err(js_err("input exceeds 10MiB"));
}
#[derive(serde::Deserialize)]
struct Input {
catalog: Catalog,
#[serde(default)]
facts: Vec<FactBlockInput>,
#[serde(default)]
rules: Vec<crate::types::Rule8>,
query: QueryInput,
receipt: crate::types::Receipt,
}
let parsed: Input =
serde_json::from_str(input_json).map_err(|e| js_err(&format!("schema rejected: {e}")))?;
let mut kernel = Kernel::new(parsed.catalog);
for block in parsed.facts {
kernel
.load_facts(block.into_fact_block())
.map_err(|c: RejectionCode| {
js_err(&format!(
"fact admission rejected: {}",
rejection_message(c)
))
})?;
}
for rule in parsed.rules {
kernel.load_rule(rule).map_err(|c: RejectionCode| {
js_err(&format!(
"rule admission rejected: {}",
rejection_message(c)
))
})?;
}
let query = parsed.query.into_query_atom();
let status = crate::replay::replay(&kernel, &query, &parsed.receipt);
to_js_str(&status)
}