use crate::check::{Checker, Report};
use crate::node::{BinOp, MatchArm, Node, NodeHash, Param, Produces};
use crate::store::Store;
use crate::ty::{Confidence, Effect, Type};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExprSpec {
Lit(i64),
Float(f64),
FloatOp {
op: BinOp,
lhs: Box<ExprSpec>,
rhs: Box<ExprSpec>,
},
IntToFloat(Box<ExprSpec>),
FloatToInt(Box<ExprSpec>),
Decimal(f64),
DecimalOp {
op: BinOp,
lhs: Box<ExprSpec>,
rhs: Box<ExprSpec>,
},
IntToDecimal(Box<ExprSpec>),
DecimalToInt(Box<ExprSpec>),
DecimalRaw(Box<ExprSpec>),
Bool(bool),
Not(Box<ExprSpec>),
Str(String),
StrLen(Box<ExprSpec>),
StrLower(Box<ExprSpec>),
StrFromCode(Box<ExprSpec>),
NumberToStr(Box<ExprSpec>),
StrToNumber(Box<ExprSpec>),
StrToNumberOpt(Box<ExprSpec>),
StrConcat(Box<ExprSpec>, Box<ExprSpec>),
StrSlice {
s: Box<ExprSpec>,
start: Box<ExprSpec>,
len: Box<ExprSpec>,
},
StrEq(Box<ExprSpec>, Box<ExprSpec>),
StrContains {
haystack: Box<ExprSpec>,
needle: Box<ExprSpec>,
},
StrStartsWith {
s: Box<ExprSpec>,
prefix: Box<ExprSpec>,
},
StrIndexOf {
haystack: Box<ExprSpec>,
needle: Box<ExprSpec>,
},
Now,
List(Vec<ExprSpec>),
ListEmpty {
elem: Type,
},
ListCons {
head: Box<ExprSpec>,
tail: Box<ExprSpec>,
},
OptionSome(Box<ExprSpec>),
OptionNone {
elem: Type,
},
OptionElse {
opt: Box<ExprSpec>,
default: Box<ExprSpec>,
},
OptionMatch {
opt: Box<ExprSpec>,
some_bind: String,
some_body: Box<ExprSpec>,
none_body: Box<ExprSpec>,
},
ListTryGet {
list: Box<ExprSpec>,
index: Box<ExprSpec>,
},
ListLen(Box<ExprSpec>),
ListGet {
list: Box<ExprSpec>,
index: Box<ExprSpec>,
},
Map(Vec<(ExprSpec, ExprSpec)>),
MapGet {
map: Box<ExprSpec>,
key: Box<ExprSpec>,
},
MapTryGet {
map: Box<ExprSpec>,
key: Box<ExprSpec>,
},
MapLen(Box<ExprSpec>),
Log(Box<ExprSpec>),
Publish(Box<ExprSpec>),
SetHeader {
name: Box<ExprSpec>,
value: Box<ExprSpec>,
},
Rand,
MutNew(Box<ExprSpec>),
MutGet(Box<ExprSpec>),
MutSet {
cell: Box<ExprSpec>,
value: Box<ExprSpec>,
},
DiskWrite {
path: Box<ExprSpec>,
content: Box<ExprSpec>,
},
DiskRead(Box<ExprSpec>),
NetGet(Box<ExprSpec>),
DbQuery {
sql: Box<ExprSpec>,
params: Box<ExprSpec>,
},
Ref(String),
Call {
func: String,
args: Vec<ExprSpec>,
},
FuncRef(String),
CallValue {
callee: Box<ExprSpec>,
args: Vec<ExprSpec>,
},
Lambda {
params: Vec<(String, Type)>,
body: Box<ExprSpec>,
},
BinOp {
op: BinOp,
lhs: Box<ExprSpec>,
rhs: Box<ExprSpec>,
},
If {
cond: Box<ExprSpec>,
then_branch: Box<ExprSpec>,
else_branch: Box<ExprSpec>,
},
Fail(String),
Handle {
body: Box<ExprSpec>,
handlers: Vec<(String, ExprSpec)>,
},
Record {
type_name: String,
fields: Vec<(String, ExprSpec)>,
},
Field {
base: Box<ExprSpec>,
type_name: String,
field: String,
},
Variant {
type_name: String,
case: String,
fields: Vec<(String, ExprSpec)>,
},
Match {
scrutinee: Box<ExprSpec>,
type_name: String,
arms: Vec<(String, Vec<String>, ExprSpec)>,
},
Hole {
expects: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct HoleInfo {
pub expects: String,
pub in_scope: Vec<String>,
pub effects_allowed: Vec<Effect>,
}
#[derive(Clone, Debug, Serialize)]
pub struct SignatureInfo {
pub name: String,
pub type_params: Vec<String>,
pub params: Vec<Param>,
pub produces: Produces,
pub requires: Vec<Effect>,
pub on_failure: Vec<String>,
pub rendered: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StepSpec {
pub binding: String,
pub value: ExprSpec,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum TypeDefSpec {
Record {
name: String,
fields: Vec<(String, Type)>,
},
Variant {
name: String,
cases: Vec<(String, Vec<(String, Type)>)>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModuleSpec {
pub name: String,
#[serde(default)]
pub types: Vec<TypeDefSpec>,
#[serde(default)]
pub functions: Vec<FunctionSpec>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FunctionSpec {
pub name: String,
#[serde(default)]
pub type_params: Vec<String>,
#[serde(default)]
pub params: Vec<Param>,
pub produces: Produces,
#[serde(default)]
pub requires: BTreeSet<Effect>,
#[serde(default)]
pub on_failure: Vec<String>,
#[serde(default)]
pub steps: Vec<StepSpec>,
pub result: ExprSpec,
}
#[derive(Debug)]
pub enum EditError {
Store(crate::store::Error),
NoTransaction,
TransactionOpen,
NoFunction,
FunctionExists,
ResultNotSet,
Lower(String),
Run(String),
NotAHole(NodeHash),
}
impl std::fmt::Display for EditError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditError::Store(e) => write!(f, "store error: {e}"),
EditError::NoTransaction => write!(f, "no edit transaction is open"),
EditError::TransactionOpen => write!(f, "an edit transaction is already open"),
EditError::NoFunction => write!(f, "no function in the draft; call create_function"),
EditError::FunctionExists => write!(f, "the draft already has a function"),
EditError::ResultNotSet => write!(f, "result not set; call set_yield before commit"),
EditError::Lower(e) => write!(f, "lower error: {e}"),
EditError::Run(e) => write!(f, "run error: {e}"),
EditError::NotAHole(h) => {
write!(f, "node {h} is not a hole; fill_hole only fills Node::Hole")
}
}
}
}
impl std::error::Error for EditError {}
impl From<crate::store::Error> for EditError {
fn from(e: crate::store::Error) -> Self {
EditError::Store(e)
}
}
type Result<T> = std::result::Result<T, EditError>;
#[derive(Clone)]
struct FunctionDraft {
name: String,
type_params: Vec<String>,
params: Vec<Param>,
produces: Produces,
requires: BTreeSet<Effect>,
on_failure: Vec<String>,
steps: Vec<(String, ExprSpec)>,
result: Option<ExprSpec>,
}
pub struct Editor {
store: Store,
txn: Option<Option<FunctionDraft>>,
}
impl Editor {
pub fn new(store: Store) -> Self {
Self { store, txn: None }
}
pub fn store(&self) -> &Store {
&self.store
}
pub fn begin_edit(&mut self) -> Result<()> {
if self.txn.is_some() {
return Err(EditError::TransactionOpen);
}
self.txn = Some(None);
Ok(())
}
pub fn abort_edit(&mut self) {
self.txn = None;
}
pub fn create_function(&mut self, name: &str) -> Result<()> {
match self.txn.as_mut() {
None => Err(EditError::NoTransaction),
Some(Some(_)) => Err(EditError::FunctionExists),
Some(slot @ None) => {
*slot = Some(FunctionDraft {
name: name.to_string(),
type_params: Vec::new(),
params: Vec::new(),
produces: Produces {
ty: Type::Number,
confidence: Confidence::Structural,
},
requires: BTreeSet::new(),
on_failure: Vec::new(),
steps: Vec::new(),
result: None,
});
Ok(())
}
}
}
fn draft(&mut self) -> Result<&mut FunctionDraft> {
match self.txn.as_mut() {
None => Err(EditError::NoTransaction),
Some(None) => Err(EditError::NoFunction),
Some(Some(d)) => Ok(d),
}
}
pub fn add_param(&mut self, name: &str, ty: Type, min_confidence: Confidence) -> Result<()> {
self.draft()?.params.push(Param {
name: name.to_string(),
ty,
min_confidence,
});
Ok(())
}
pub fn set_type_params(&mut self, type_params: Vec<String>) -> Result<()> {
self.draft()?.type_params = type_params;
Ok(())
}
pub fn set_produces(&mut self, ty: Type, confidence: Confidence) -> Result<()> {
self.draft()?.produces = Produces { ty, confidence };
Ok(())
}
pub fn set_effects(&mut self, effects: BTreeSet<Effect>) -> Result<()> {
self.draft()?.requires = effects;
Ok(())
}
pub fn set_on_failure(&mut self, failures: Vec<String>) -> Result<()> {
self.draft()?.on_failure = failures;
Ok(())
}
pub fn add_step(&mut self, binding: &str, value: ExprSpec) -> Result<()> {
self.draft()?.steps.push((binding.to_string(), value));
Ok(())
}
pub fn set_yield(&mut self, value: ExprSpec) -> Result<()> {
self.draft()?.result = Some(value);
Ok(())
}
pub fn describe_hole(&mut self) -> Result<HoleInfo> {
let d = self.draft()?;
let mut in_scope: Vec<String> = d.params.iter().map(|p| p.name.clone()).collect();
in_scope.extend(d.steps.iter().map(|(b, _)| b.clone()));
Ok(HoleInfo {
expects: format!("{:?}", d.produces.ty),
in_scope,
effects_allowed: d.requires.iter().copied().collect(),
})
}
pub fn commit_edit(&mut self) -> Result<(NodeHash, Report)> {
let draft = match self.txn.take() {
None => return Err(EditError::NoTransaction),
Some(None) => return Err(EditError::NoFunction),
Some(Some(d)) => d,
};
let result_spec = draft.result.as_ref().ok_or(EditError::ResultNotSet)?;
let mut body = Vec::with_capacity(draft.steps.len());
for (binding, spec) in &draft.steps {
let value = self.put_expr(spec)?;
body.push(self.store.put(&Node::Step {
binding: binding.clone(),
value,
})?);
}
let result = self.put_expr(result_spec)?;
let fn_hash = self.store.put(&Node::Function {
name: draft.name.clone(),
type_params: draft.type_params.clone(),
params: draft.params.clone(),
produces: draft.produces.clone(),
requires: draft.requires.clone(),
on_failure: draft.on_failure.clone(),
body,
result,
})?;
let report = Checker::new(&self.store).check(&fn_hash)?;
Ok((fn_hash, report))
}
pub fn apply_function(&mut self, spec: &FunctionSpec) -> Result<(NodeHash, Report)> {
self.begin_edit()?;
self.create_function(&spec.name)?;
self.set_type_params(spec.type_params.clone())?;
for p in &spec.params {
self.add_param(&p.name, p.ty.clone(), p.min_confidence)?;
}
self.set_produces(spec.produces.ty.clone(), spec.produces.confidence)?;
self.set_effects(spec.requires.clone())?;
self.set_on_failure(spec.on_failure.clone())?;
for s in &spec.steps {
self.add_step(&s.binding, s.value.clone())?;
}
self.set_yield(spec.result.clone())?;
self.commit_edit()
}
pub fn define_type(&self, spec: &TypeDefSpec) -> Result<NodeHash> {
let node = match spec {
TypeDefSpec::Record { name, fields } => Node::RecordDef {
name: name.clone(),
fields: fields.clone(),
},
TypeDefSpec::Variant { name, cases } => Node::VariantDef {
name: name.clone(),
cases: cases.clone(),
},
};
Ok(self.store.put(&node)?)
}
fn materialize_function(&self, spec: &FunctionSpec) -> Result<NodeHash> {
let mut body = Vec::with_capacity(spec.steps.len());
for s in &spec.steps {
let value = self.put_expr(&s.value)?;
body.push(self.store.put(&Node::Step {
binding: s.binding.clone(),
value,
})?);
}
let result = self.put_expr(&spec.result)?;
Ok(self.store.put(&Node::Function {
name: spec.name.clone(),
type_params: spec.type_params.clone(),
params: spec.params.clone(),
produces: spec.produces.clone(),
requires: spec.requires.clone(),
on_failure: spec.on_failure.clone(),
body,
result,
})?)
}
pub fn apply_module(&self, spec: &ModuleSpec) -> Result<(NodeHash, Report)> {
let mut types = Vec::with_capacity(spec.types.len());
for t in &spec.types {
types.push(self.define_type(t)?);
}
let mut functions = Vec::with_capacity(spec.functions.len());
for f in &spec.functions {
functions.push(self.materialize_function(f)?);
}
let module = self.store.put(&Node::Module {
name: spec.name.clone(),
types,
functions,
})?;
let report = Checker::new(&self.store).check(&module)?;
Ok((module, report))
}
pub fn run_module(
&self,
module: &NodeHash,
name: &str,
args: &[i64],
) -> Result<i64> {
let wasm = crate::wasm::lower(&self.store, module)
.map_err(|e| EditError::Lower(e.to_string()))?;
crate::wasm::run_i64(&wasm, name, args)
.map_err(|e| EditError::Run(e.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub fn run_handler(
&self,
module: &NodeHash,
handler: &str,
db_path: &str,
method: &str,
path: &str,
body: &str,
headers: &str,
) -> Result<crate::wasm::HttpResponse> {
let wasm = crate::wasm::lower(&self.store, module)
.map_err(|e| EditError::Lower(e.to_string()))?;
crate::wasm::serve_request_db_h(
&wasm, handler, db_path, method, path, body, headers,
)
.map_err(|e| EditError::Run(e.to_string()))
}
pub fn query_type(
&self,
module: &NodeHash,
name: &str,
) -> Result<Option<SignatureInfo>> {
let Some(Node::Module { functions, .. }) = self.store.get(module)?
else {
return Ok(None);
};
for fh in &functions {
if let Some(Node::Function {
name: n,
type_params,
params,
produces,
requires,
on_failure,
..
}) = self.store.get(fh)?
{
if n == name {
let rendered = crate::render::render(&self.store, fh)
.unwrap_or_else(|_| String::new());
return Ok(Some(SignatureInfo {
name: n,
type_params,
params,
produces,
requires: requires.into_iter().collect(),
on_failure,
rendered,
}));
}
}
}
Ok(None)
}
pub fn find_references(
&self,
root: &NodeHash,
target: &NodeHash,
) -> Result<Vec<String>> {
let mut refs: BTreeSet<String> = BTreeSet::new();
let mut seen: std::collections::HashSet<NodeHash> =
std::collections::HashSet::new();
let mut stack = vec![root.clone()];
while let Some(h) = stack.pop() {
if !seen.insert(h.clone()) {
continue;
}
let Some(node) = self.store.get(&h)? else {
continue;
};
for child in crate::check::child_hashes(&node) {
if child == target {
refs.insert(h.to_string());
}
stack.push(child.clone());
}
}
Ok(refs.into_iter().collect())
}
pub fn replace_node(
&self,
root: &NodeHash,
target: &NodeHash,
replacement: &NodeHash,
) -> Result<(NodeHash, Report)> {
let mut cache: std::collections::HashMap<NodeHash, NodeHash> =
std::collections::HashMap::new();
let new_root = self.rewrite(root, target, replacement, &mut cache)?;
let report = Checker::new(&self.store).check(&new_root)?;
Ok((new_root, report))
}
fn rewrite(
&self,
h: &NodeHash,
target: &NodeHash,
replacement: &NodeHash,
cache: &mut std::collections::HashMap<NodeHash, NodeHash>,
) -> Result<NodeHash> {
if h == target {
return Ok(replacement.clone());
}
if let Some(c) = cache.get(h) {
return Ok(c.clone());
}
let Some(node) = self.store.get(h)? else {
return Ok(h.clone());
};
let kids = crate::check::child_hashes(&node);
let mut new_kids = Vec::with_capacity(kids.len());
let mut changed = false;
for c in kids {
let nc = self.rewrite(c, target, replacement, cache)?;
if &nc != c {
changed = true;
}
new_kids.push(nc);
}
let out = if changed {
let rebuilt = crate::check::with_child_hashes(&node, &new_kids);
self.store.put(&rebuilt)?
} else {
h.clone()
};
cache.insert(h.clone(), out.clone());
Ok(out)
}
pub fn fill_hole(
&self,
root: &NodeHash,
hole: &NodeHash,
replacement: &NodeHash,
) -> Result<(NodeHash, Report)> {
match self.store.get(hole)? {
Some(Node::Hole { .. }) => {}
_ => return Err(EditError::NotAHole(hole.clone())),
}
let (candidate, report) = self.replace_node(root, hole, replacement)?;
if report.ok() {
Ok((candidate, report))
} else {
Ok((root.clone(), report))
}
}
pub fn put_expr(&self, spec: &ExprSpec) -> std::result::Result<NodeHash, crate::store::Error> {
let node = match spec {
ExprSpec::Lit(v) => Node::Lit(*v),
ExprSpec::Float(f) => Node::FloatLit(f.to_bits()),
ExprSpec::FloatOp { op, lhs, rhs } => Node::FloatOp {
op: *op,
lhs: self.put_expr(lhs)?,
rhs: self.put_expr(rhs)?,
},
ExprSpec::IntToFloat(a) => Node::IntToFloat(self.put_expr(a)?),
ExprSpec::FloatToInt(a) => Node::FloatToInt(self.put_expr(a)?),
ExprSpec::Decimal(v) => {
Node::DecimalLit((v * 10_000.0).round() as i64)
}
ExprSpec::DecimalOp { op, lhs, rhs } => Node::DecimalOp {
op: *op,
lhs: self.put_expr(lhs)?,
rhs: self.put_expr(rhs)?,
},
ExprSpec::IntToDecimal(a) => Node::IntToDecimal(self.put_expr(a)?),
ExprSpec::DecimalToInt(a) => Node::DecimalToInt(self.put_expr(a)?),
ExprSpec::DecimalRaw(a) => Node::DecimalRaw(self.put_expr(a)?),
ExprSpec::Bool(b) => Node::Bool(*b),
ExprSpec::Not(a) => Node::Not(self.put_expr(a)?),
ExprSpec::Str(s) => Node::Str(s.clone()),
ExprSpec::StrLen(a) => Node::StrLen(self.put_expr(a)?),
ExprSpec::StrLower(a) => Node::StrLower(self.put_expr(a)?),
ExprSpec::StrFromCode(a) => {
Node::StrFromCode(self.put_expr(a)?)
}
ExprSpec::NumberToStr(a) => Node::NumberToStr(self.put_expr(a)?),
ExprSpec::StrToNumber(a) => Node::StrToNumber(self.put_expr(a)?),
ExprSpec::StrToNumberOpt(a) => {
Node::StrToNumberOpt(self.put_expr(a)?)
}
ExprSpec::StrConcat(a, b) => {
Node::StrConcat(self.put_expr(a)?, self.put_expr(b)?)
}
ExprSpec::StrSlice { s, start, len } => Node::StrSlice {
s: self.put_expr(s)?,
start: self.put_expr(start)?,
len: self.put_expr(len)?,
},
ExprSpec::StrEq(a, b) => {
Node::StrEq(self.put_expr(a)?, self.put_expr(b)?)
}
ExprSpec::StrContains { haystack, needle } => Node::StrContains {
haystack: self.put_expr(haystack)?,
needle: self.put_expr(needle)?,
},
ExprSpec::StrStartsWith { s, prefix } => Node::StrStartsWith {
s: self.put_expr(s)?,
prefix: self.put_expr(prefix)?,
},
ExprSpec::StrIndexOf { haystack, needle } => Node::StrIndexOf {
haystack: self.put_expr(haystack)?,
needle: self.put_expr(needle)?,
},
ExprSpec::Now => Node::Now,
ExprSpec::List(es) => {
let mut hs = Vec::with_capacity(es.len());
for e in es {
hs.push(self.put_expr(e)?);
}
Node::List(hs)
}
ExprSpec::ListEmpty { elem } => Node::ListEmpty { elem: elem.clone() },
ExprSpec::ListCons { head, tail } => Node::ListCons {
head: self.put_expr(head)?,
tail: self.put_expr(tail)?,
},
ExprSpec::OptionSome(v) => Node::OptionSome(self.put_expr(v)?),
ExprSpec::OptionNone { elem } => Node::OptionNone {
elem: elem.clone(),
},
ExprSpec::OptionElse { opt, default } => Node::OptionElse {
opt: self.put_expr(opt)?,
default: self.put_expr(default)?,
},
ExprSpec::OptionMatch {
opt,
some_bind,
some_body,
none_body,
} => Node::OptionMatch {
opt: self.put_expr(opt)?,
some_bind: some_bind.clone(),
some_body: self.put_expr(some_body)?,
none_body: self.put_expr(none_body)?,
},
ExprSpec::ListTryGet { list, index } => Node::ListTryGet {
list: self.put_expr(list)?,
index: self.put_expr(index)?,
},
ExprSpec::ListLen(a) => Node::ListLen(self.put_expr(a)?),
ExprSpec::ListGet { list, index } => Node::ListGet {
list: self.put_expr(list)?,
index: self.put_expr(index)?,
},
ExprSpec::Map(pairs) => {
let mut hs = Vec::with_capacity(pairs.len());
for (k, v) in pairs {
hs.push((self.put_expr(k)?, self.put_expr(v)?));
}
Node::Map(hs)
}
ExprSpec::MapGet { map, key } => Node::MapGet {
map: self.put_expr(map)?,
key: self.put_expr(key)?,
},
ExprSpec::MapTryGet { map, key } => Node::MapTryGet {
map: self.put_expr(map)?,
key: self.put_expr(key)?,
},
ExprSpec::MapLen(a) => Node::MapLen(self.put_expr(a)?),
ExprSpec::Log(a) => Node::Log(self.put_expr(a)?),
ExprSpec::Publish(a) => Node::Publish(self.put_expr(a)?),
ExprSpec::SetHeader { name, value } => Node::SetHeader {
name: self.put_expr(name)?,
value: self.put_expr(value)?,
},
ExprSpec::Rand => Node::Rand,
ExprSpec::MutNew(v) => Node::MutNew(self.put_expr(v)?),
ExprSpec::MutGet(cl) => Node::MutGet(self.put_expr(cl)?),
ExprSpec::MutSet { cell, value } => Node::MutSet {
cell: self.put_expr(cell)?,
value: self.put_expr(value)?,
},
ExprSpec::DiskWrite { path, content } => Node::DiskWrite {
path: self.put_expr(path)?,
content: self.put_expr(content)?,
},
ExprSpec::DiskRead(p) => Node::DiskRead(self.put_expr(p)?),
ExprSpec::NetGet(u) => Node::NetGet(self.put_expr(u)?),
ExprSpec::DbQuery { sql, params } => Node::DbQuery {
sql: self.put_expr(sql)?,
params: self.put_expr(params)?,
},
ExprSpec::Ref(n) => Node::Ref(n.clone()),
ExprSpec::Hole { expects } => Node::Hole {
expects: expects.clone(),
},
ExprSpec::Call { func, args } => {
let mut hs = Vec::with_capacity(args.len());
for a in args {
hs.push(self.put_expr(a)?);
}
Node::Call {
func: func.clone(),
args: hs,
}
}
ExprSpec::Lambda { params, body } => {
let b = self.put_expr(body)?;
Node::Lambda {
params: params
.iter()
.map(|(n, t)| Param {
name: n.clone(),
ty: t.clone(),
min_confidence: Confidence::External,
})
.collect(),
body: b,
}
}
ExprSpec::FuncRef(name) => Node::FuncRef(name.clone()),
ExprSpec::CallValue { callee, args } => {
let cl = self.put_expr(callee)?;
let mut hs = Vec::with_capacity(args.len());
for a in args {
hs.push(self.put_expr(a)?);
}
Node::CallValue {
callee: cl,
args: hs,
}
}
ExprSpec::BinOp { op, lhs, rhs } => {
let l = self.put_expr(lhs)?;
let r = self.put_expr(rhs)?;
Node::BinOp {
op: *op,
lhs: l,
rhs: r,
}
}
ExprSpec::If {
cond,
then_branch,
else_branch,
} => {
let c = self.put_expr(cond)?;
let t = self.put_expr(then_branch)?;
let e = self.put_expr(else_branch)?;
Node::If {
cond: c,
then_branch: t,
else_branch: e,
}
}
ExprSpec::Fail(v) => Node::Fail(v.clone()),
ExprSpec::Handle { body, handlers } => {
let b = self.put_expr(body)?;
let mut hs = Vec::with_capacity(handlers.len());
for (variant, recover) in handlers {
hs.push((variant.clone(), self.put_expr(recover)?));
}
Node::Handle {
body: b,
handlers: hs,
}
}
ExprSpec::Record { type_name, fields } => {
let mut fs = Vec::with_capacity(fields.len());
for (n, v) in fields {
fs.push((n.clone(), self.put_expr(v)?));
}
Node::Record {
type_name: type_name.clone(),
fields: fs,
}
}
ExprSpec::Field {
base,
type_name,
field,
} => {
let b = self.put_expr(base)?;
Node::Field {
base: b,
type_name: type_name.clone(),
field: field.clone(),
}
}
ExprSpec::Variant {
type_name,
case,
fields,
} => {
let mut fs = Vec::with_capacity(fields.len());
for (n, v) in fields {
fs.push((n.clone(), self.put_expr(v)?));
}
Node::Variant {
type_name: type_name.clone(),
case: case.clone(),
fields: fs,
}
}
ExprSpec::Match {
scrutinee,
type_name,
arms,
} => {
let sc = self.put_expr(scrutinee)?;
let mut ms = Vec::with_capacity(arms.len());
for (case, bindings, body) in arms {
ms.push(MatchArm {
case: case.clone(),
bindings: bindings.clone(),
body: self.put_expr(body)?,
});
}
Node::Match {
scrutinee: sc,
type_name: type_name.clone(),
arms: ms,
}
}
};
self.store.put(&node)
}
pub fn run(&self, func: &NodeHash, name: &str, args: &[i64]) -> Result<i64> {
let module = self.store.put(&Node::Module {
name: "main".into(),
types: vec![],
functions: vec![func.clone()],
})?;
let wasm = crate::wasm::lower(&self.store, &module)
.map_err(|e| EditError::Lower(e.to_string()))?;
crate::wasm::run_i64(&wasm, name, args).map_err(|e| EditError::Run(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::check::Status;
#[test]
fn worked_session_author_check_and_run() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
e.create_function("id").unwrap();
e.add_param("n", Type::Number, Confidence::External).unwrap();
e.set_produces(Type::Number, Confidence::External).unwrap();
e.set_effects(BTreeSet::new()).unwrap(); e.set_yield(ExprSpec::Ref("n".into())).unwrap();
let (h, report) = e.commit_edit().unwrap();
assert!(report.ok(), "violations: {:?}", report.violations);
assert_eq!(report.status, Status::Complete);
assert_eq!(e.run(&h, "id", &[7]).unwrap(), 7);
}
#[test]
fn transactions_are_non_nesting() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
assert!(matches!(e.begin_edit(), Err(EditError::TransactionOpen)));
}
#[test]
fn abort_discards_the_draft() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
e.create_function("f").unwrap();
e.abort_edit();
assert!(matches!(e.commit_edit(), Err(EditError::NoTransaction)));
}
#[test]
fn ops_require_an_open_transaction() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
assert!(matches!(
e.create_function("f"),
Err(EditError::NoTransaction)
));
}
#[test]
fn describe_hole_reports_scope_and_effects() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
e.create_function("f").unwrap();
e.add_param("a", Type::Number, Confidence::Structural)
.unwrap();
e.add_step("b", ExprSpec::Lit(1)).unwrap();
let mut db = BTreeSet::new();
db.insert(Effect::Db);
e.set_effects(db).unwrap();
let info = e.describe_hole().unwrap();
assert_eq!(info.in_scope, vec!["a".to_string(), "b".to_string()]);
assert_eq!(info.effects_allowed, vec![Effect::Db]);
assert_eq!(info.expects, "Number");
}
#[test]
fn committing_with_a_hole_is_incomplete_but_not_invalid_and_will_not_run() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
e.create_function("f").unwrap();
e.set_produces(Type::Number, Confidence::Structural).unwrap();
e.set_yield(ExprSpec::Hole {
expects: "Number".into(),
})
.unwrap();
let (h, report) = e.commit_edit().unwrap();
assert_eq!(report.status, Status::Incomplete);
assert!(report.ok()); assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
}
#[test]
fn apply_function_matches_fine_grained() {
let mut e1 = Editor::new(Store::open_in_memory().unwrap());
e1.begin_edit().unwrap();
e1.create_function("id").unwrap();
e1.add_param("n", Type::Number, Confidence::External).unwrap();
e1.set_produces(Type::Number, Confidence::External).unwrap();
e1.set_effects(BTreeSet::new()).unwrap();
e1.set_yield(ExprSpec::Ref("n".into())).unwrap();
let (h1, r1) = e1.commit_edit().unwrap();
let spec = FunctionSpec {
name: "id".into(),
type_params: vec![],
params: vec![Param {
name: "n".into(),
ty: Type::Number,
min_confidence: Confidence::External,
}],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![],
result: ExprSpec::Ref("n".into()),
};
let mut e2 = Editor::new(Store::open_in_memory().unwrap());
let (h2, r2) = e2.apply_function(&spec).unwrap();
assert_eq!(h1, h2);
assert_eq!(r1.ok(), r2.ok());
assert_eq!(r1.status, r2.status);
}
#[test]
fn commit_without_a_result_errors() {
let s = Store::open_in_memory().unwrap();
let mut e = Editor::new(s);
e.begin_edit().unwrap();
e.create_function("f").unwrap();
assert!(matches!(e.commit_edit(), Err(EditError::ResultNotSet)));
}
#[test]
fn module_authoring_composes_types_and_functions() {
let ext_num = || Produces {
ty: Type::Number,
confidence: Confidence::External,
};
let p = |name: &str, ty: Type| Param {
name: name.into(),
ty,
min_confidence: Confidence::External,
};
let spec = ModuleSpec {
name: "m".into(),
types: vec![
TypeDefSpec::Record {
name: "Box".into(),
fields: vec![("v".into(), Type::Number)],
},
TypeDefSpec::Variant {
name: "Opt".into(),
cases: vec![
("Some".into(), vec![("v".into(), Type::Number)]),
("None".into(), vec![]),
],
},
],
functions: vec![
FunctionSpec {
name: "demo".into(),
type_params: vec![],
params: vec![p("n", Type::Number)],
produces: ext_num(),
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![
StepSpec {
binding: "b".into(),
value: ExprSpec::Call {
func: "mk".into(),
args: vec![ExprSpec::Ref("n".into())],
},
},
StepSpec {
binding: "x".into(),
value: ExprSpec::Call {
func: "unwrap".into(),
args: vec![ExprSpec::Ref("b".into())],
},
},
],
result: ExprSpec::Call {
func: "get_or".into(),
args: vec![
ExprSpec::Variant {
type_name: "Opt".into(),
case: "Some".into(),
fields: vec![(
"v".into(),
ExprSpec::Ref("x".into()),
)],
},
ExprSpec::Lit(0),
],
},
},
FunctionSpec {
name: "mk".into(),
type_params: vec![],
params: vec![p("n", Type::Number)],
produces: Produces {
ty: Type::Named("Box".into()),
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![],
result: ExprSpec::Record {
type_name: "Box".into(),
fields: vec![("v".into(), ExprSpec::Ref("n".into()))],
},
},
FunctionSpec {
name: "unwrap".into(),
type_params: vec![],
params: vec![p("b", Type::Named("Box".into()))],
produces: ext_num(),
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![],
result: ExprSpec::Field {
base: Box::new(ExprSpec::Ref("b".into())),
type_name: "Box".into(),
field: "v".into(),
},
},
FunctionSpec {
name: "get_or".into(),
type_params: vec![],
params: vec![
p("o", Type::Named("Opt".into())),
p("d", Type::Number),
],
produces: ext_num(),
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![],
result: ExprSpec::Match {
scrutinee: Box::new(ExprSpec::Ref("o".into())),
type_name: "Opt".into(),
arms: vec![
(
"Some".into(),
vec!["v".into()],
ExprSpec::Ref("v".into()),
),
("None".into(), vec![], ExprSpec::Ref("d".into())),
],
},
},
],
};
let e = Editor::new(Store::open_in_memory().unwrap());
let (module, report) = e.apply_module(&spec).unwrap();
assert!(report.ok(), "violations: {:?}", report.violations);
assert_eq!(report.status, Status::Complete);
assert_eq!(e.run_module(&module, "demo", &[7]).unwrap(), 7);
}
#[test]
fn find_references_is_the_structural_reference_set() {
let s = Store::open_in_memory().unwrap();
let two = s.put(&Node::Lit(2)).unwrap();
let a = s
.put(&Node::BinOp {
op: BinOp::Add,
lhs: two.clone(),
rhs: two.clone(),
})
.unwrap();
let b = s
.put(&Node::BinOp {
op: BinOp::Mul,
lhs: a.clone(),
rhs: two.clone(),
})
.unwrap();
let root = s
.put(&Node::BinOp {
op: BinOp::Sub,
lhs: b.clone(),
rhs: a.clone(),
})
.unwrap();
let e = Editor::new(s);
let mut want_a = vec![b.to_string(), root.to_string()];
want_a.sort();
assert_eq!(e.find_references(&root, &a).unwrap(), want_a);
let mut want_two = vec![a.to_string(), b.to_string()];
want_two.sort();
assert_eq!(e.find_references(&root, &two).unwrap(), want_two);
assert!(e.find_references(&root, &root).unwrap().is_empty());
}
#[test]
fn with_child_hashes_round_trips() {
use crate::check::{child_hashes, with_child_hashes};
let s = Store::open_in_memory().unwrap();
let h = s.put(&Node::Lit(1)).unwrap();
let h2 = s.put(&Node::Lit(2)).unwrap();
let h3 = s.put(&Node::Lit(3)).unwrap();
let prod = Produces {
ty: Type::Number,
confidence: Confidence::External,
};
let nodes = vec![
Node::Lit(7),
Node::Now,
Node::Ref("x".into()),
Node::Hole { expects: "Number".into() },
Node::Not(h.clone()),
Node::StrConcat(h.clone(), h2.clone()),
Node::StrSlice { s: h.clone(), start: h2.clone(), len: h3.clone() },
Node::If {
cond: h.clone(),
then_branch: h2.clone(),
else_branch: h3.clone(),
},
Node::BinOp { op: BinOp::Add, lhs: h.clone(), rhs: h2.clone() },
Node::List(vec![h.clone(), h2.clone(), h3.clone()]),
Node::Map(vec![(h.clone(), h2.clone()), (h3.clone(), h.clone())]),
Node::Record {
type_name: "R".into(),
fields: vec![("x".into(), h.clone()), ("y".into(), h2.clone())],
},
Node::Variant {
type_name: "V".into(),
case: "C".into(),
fields: vec![("a".into(), h.clone())],
},
Node::Field {
base: h.clone(),
type_name: "R".into(),
field: "x".into(),
},
Node::OptionMatch {
opt: h.clone(),
some_bind: "v".into(),
some_body: h2.clone(),
none_body: h3.clone(),
},
Node::Match {
scrutinee: h.clone(),
type_name: "V".into(),
arms: vec![
MatchArm { case: "C".into(), bindings: vec!["a".into()], body: h2.clone() },
MatchArm { case: "D".into(), bindings: vec![], body: h3.clone() },
],
},
Node::Handle {
body: h.clone(),
handlers: vec![("E".into(), h2.clone()), ("F".into(), h3.clone())],
},
Node::Call { func: "g".into(), args: vec![h.clone(), h2.clone()] },
Node::CallValue { callee: h.clone(), args: vec![h2.clone(), h3.clone()] },
Node::Lambda { params: vec![], body: h.clone() },
Node::Step { binding: "b".into(), value: h.clone() },
Node::Function {
name: "f".into(),
type_params: vec![],
params: vec![],
produces: prod.clone(),
requires: BTreeSet::new(),
on_failure: vec![],
body: vec![h.clone(), h2.clone()],
result: h3.clone(),
},
Node::Module {
name: "m".into(),
types: vec![h.clone()],
functions: vec![h2.clone(), h3.clone()],
},
];
for n in nodes {
let kids: Vec<NodeHash> =
child_hashes(&n).into_iter().cloned().collect();
assert_eq!(
with_child_hashes(&n, &kids),
n,
"round-trip failed for {n:?}"
);
}
}
#[test]
fn replace_node_rehashes_and_rechecks() {
let spec = ModuleSpec {
name: "m".into(),
types: vec![],
functions: vec![FunctionSpec {
name: "f".into(),
type_params: vec![],
params: vec![],
produces: Produces {
ty: Type::Number,
confidence: Confidence::External,
},
requires: BTreeSet::new(),
on_failure: vec![],
steps: vec![],
result: ExprSpec::BinOp {
op: BinOp::Add,
lhs: Box::new(ExprSpec::Lit(2)),
rhs: Box::new(ExprSpec::Lit(3)),
},
}],
};
let e = Editor::new(Store::open_in_memory().unwrap());
let (m, report) = e.apply_module(&spec).unwrap();
assert!(report.ok());
assert_eq!(e.run_module(&m, "f", &[]).unwrap(), 5);
let three = e.store().put(&Node::Lit(3)).unwrap();
let four = e.store().put(&Node::Lit(4)).unwrap();
let (m2, r2) = e.replace_node(&m, &three, &four).unwrap();
assert!(r2.ok() && r2.status == Status::Complete);
assert_ne!(m2, m, "the spine must rehash");
assert_eq!(e.run_module(&m2, "f", &[]).unwrap(), 6, "2 + 4");
assert_eq!(e.run_module(&m, "f", &[]).unwrap(), 5);
let absent = e.store().put(&Node::Lit(999)).unwrap();
let (m3, _) = e.replace_node(&m, &absent, &four).unwrap();
assert_eq!(m3, m, "no-op replace returns the shared root");
}
#[test]
fn fill_hole_accepts_only_a_valid_fill_else_the_hole_remains() {
let mut e = Editor::new(Store::open_in_memory().unwrap());
e.begin_edit().unwrap();
e.create_function("f").unwrap();
e.set_produces(Type::Number, Confidence::Structural).unwrap();
e.set_effects(BTreeSet::new()).unwrap();
e.set_yield(ExprSpec::Hole {
expects: "Number".into(),
})
.unwrap();
let (h, report) = e.commit_edit().unwrap();
assert_eq!(report.status, Status::Incomplete);
let hole = e
.store()
.put(&Node::Hole {
expects: "Number".into(),
})
.unwrap();
assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
let bad = e.store().put(&Node::Str("x".into())).unwrap();
let (hr, rr) = e.fill_hole(&h, &hole, &bad).unwrap();
assert!(!rr.ok(), "a contract-violating fill must be rejected");
assert!(!rr.violations.is_empty(), "the principle is reported");
assert_eq!(hr, h, "rejected → the hole remains, root unchanged");
assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
let good = e.store().put(&Node::Lit(42)).unwrap();
let (h2, r2) = e.fill_hole(&h, &hole, &good).unwrap();
assert!(r2.ok() && r2.status == Status::Complete);
assert_ne!(h2, h);
assert_eq!(e.run(&h2, "f", &[]).unwrap(), 42);
assert!(matches!(e.run(&h, "f", &[]), Err(EditError::Lower(_))));
assert!(matches!(
e.fill_hole(&h, &good, &good),
Err(EditError::NotAHole(_))
));
}
}