fn expr_supports_forced_effect_site(expr: &Expr) -> bool {
matches!(expr, Expr::ReceiverCall { .. } | Expr::Await(_))
|| matches!(
expr,
Expr::ResultUnwrap(inner)
if matches!(inner.as_ref(), Expr::ReceiverCall { .. } | Expr::Await(_))
)
}
fn intrinsic_for_builtin(name: &str, argc: usize) -> Option<IntrinsicOp> {
Some(match name {
"len" => IntrinsicOp::Len,
"empty" => IntrinsicOp::Empty,
"keys" => IntrinsicOp::Keys,
"values" => IntrinsicOp::Values,
"contains" => IntrinsicOp::Contains,
"find" => IntrinsicOp::Find(argc),
"grep_text" => IntrinsicOp::GrepText,
"starts_with" => IntrinsicOp::StartsWith,
"ends_with" => IntrinsicOp::EndsWith,
"split" => IntrinsicOp::Split,
"join" => IntrinsicOp::Join,
"trim" => IntrinsicOp::Trim,
"slice" => IntrinsicOp::Slice,
"to_string" => IntrinsicOp::ToString,
"to_int" => IntrinsicOp::ToInt,
"to_float" => IntrinsicOp::ToFloat,
"json_parse" => IntrinsicOp::JsonParse,
"format" => IntrinsicOp::Format(argc),
"validate" => IntrinsicOp::Validate,
"range" => IntrinsicOp::Range(argc),
"ceil_div" => IntrinsicOp::CeilDiv,
"floor_div" => IntrinsicOp::FloorDiv,
"push" => IntrinsicOp::Push,
_ => return None,
})
}
fn expr_key(expr: &Expr) -> usize {
expr as *const Expr as usize
}
fn lashlang_execution_paths(program: &Program) -> FxHashMap<usize, LashlangAstPath> {
let mut paths = FxHashMap::default();
collect_lashlang_execution_paths(&program.main, LashlangAstPath::root(), &mut paths);
paths
}
fn collect_lashlang_execution_paths(
expr: &Expr,
path: LashlangAstPath,
paths: &mut FxHashMap<usize, LashlangAstPath>,
) {
paths.insert(expr_key(expr), path.clone());
if let Expr::LabelAnnotated { expr, .. } = expr {
collect_lashlang_execution_paths(expr, path, paths);
return;
}
for (index, child) in expr.children().enumerate() {
collect_lashlang_execution_paths(child, path.child(index), paths);
}
}
fn label_attaches_to_concrete_node(expr: &Expr) -> bool {
match expr {
Expr::LabelAnnotated { .. } => false,
Expr::Assign { expr, .. } => label_attaches_to_assignment_value(expr),
Expr::Await(expr) | Expr::ResultUnwrap(expr) => label_attaches_to_concrete_node(expr),
Expr::ReceiverCall { .. }
| Expr::StartProcess(_)
| Expr::SleepFor(_)
| Expr::SleepUntil(_)
| Expr::WaitSignal { .. }
| Expr::SignalRun { .. }
| Expr::Submit(_)
| Expr::Yield(_)
| Expr::Wake(_)
| Expr::Finish(_)
| Expr::Fail(_)
| Expr::If { .. } => true,
Expr::Block(_)
| Expr::Null
| Expr::Bool(_)
| Expr::Number(_)
| Expr::String(_)
| Expr::Variable(_)
| Expr::List(_)
| Expr::Record(_)
| Expr::For { .. }
| Expr::While { .. }
| Expr::Break
| Expr::Continue
| Expr::ProcessRef { .. }
| Expr::HostDescriptorConstructor { .. }
| Expr::ResourceRef(_)
| Expr::Cancel(_)
| Expr::Print(_)
| Expr::BuiltinCall { .. }
| Expr::Field { .. }
| Expr::Index { .. }
| Expr::Unary { .. }
| Expr::Binary { .. }
| Expr::TypeLiteral(_) => false,
}
}
fn label_attaches_to_assignment_value(expr: &Expr) -> bool {
match expr {
Expr::Await(expr) | Expr::ResultUnwrap(expr) => label_attaches_to_assignment_value(expr),
Expr::ReceiverCall { .. }
| Expr::StartProcess(_)
| Expr::SleepFor(_)
| Expr::SleepUntil(_)
| Expr::WaitSignal { .. }
| Expr::SignalRun { .. }
| Expr::Submit(_)
| Expr::Yield(_)
| Expr::Wake(_)
| Expr::Finish(_)
| Expr::Fail(_)
| Expr::If { .. } => true,
_ => false,
}
}
pub(crate) fn is_pure_expr(expr: &Expr) -> bool {
match expr {
Expr::LabelAnnotated { expr, .. } => is_pure_expr(expr),
Expr::Null
| Expr::Bool(_)
| Expr::Number(_)
| Expr::String(_)
| Expr::Variable(_)
| Expr::ProcessRef { .. }
| Expr::ResourceRef(_) => true,
Expr::List(items) => items.iter().all(is_pure_expr),
Expr::Record(entries) => entries.iter().all(|(_, value)| is_pure_expr(value)),
Expr::ResultUnwrap(expr) => is_pure_expr(expr),
Expr::HostDescriptorConstructor { input, .. } => is_pure_expr(input),
Expr::BuiltinCall { args, .. } => args.iter().all(is_pure_expr),
Expr::Field { target, .. } => is_pure_expr(target),
Expr::Index { target, index } => is_pure_expr(target) && is_pure_expr(index),
Expr::Unary { expr, .. } => is_pure_expr(expr),
Expr::If {
condition,
then_block,
else_block,
} => is_pure_expr(condition) && is_pure_expr(then_block) && is_pure_expr(else_block),
Expr::Binary { left, right, .. } => is_pure_expr(left) && is_pure_expr(right),
Expr::TypeLiteral(ty) => fold_type(ty).is_some(),
Expr::Block(_)
| Expr::Assign { .. }
| Expr::For { .. }
| Expr::While { .. }
| Expr::Break
| Expr::Continue
| Expr::ReceiverCall { .. }
| Expr::StartProcess(_)
| Expr::Await(_)
| Expr::SleepFor(_)
| Expr::SleepUntil(_)
| Expr::WaitSignal { .. }
| Expr::SignalRun { .. }
| Expr::Cancel(_)
| Expr::Print(_)
| Expr::Submit(_)
| Expr::Yield(_)
| Expr::Wake(_)
| Expr::Finish(_)
| Expr::Fail(_) => false,
}
}
fn contains_type_literal(expr: &Expr) -> bool {
matches!(expr, Expr::TypeLiteral(_)) || expr.children().any(contains_type_literal)
}
mod schema_keys {
pub(super) const TYPE: &str = "type";
pub(super) const ITEMS: &str = "items";
pub(super) const PROPERTIES: &str = "properties";
pub(super) const REQUIRED: &str = "required";
pub(super) const ADDITIONAL_PROPERTIES: &str = "additionalProperties";
pub(super) const ANY_OF: &str = "anyOf";
pub(super) const ENUM: &str = "enum";
pub(super) const ARRAY: &str = "array";
pub(super) const OBJECT: &str = "object";
pub(super) const STRING: &str = "string";
}
fn fold_type(ty: &TypeExpr) -> Option<Value> {
use schema_keys::*;
match ty {
TypeExpr::Any => Some(interned_scalar_schema(ScalarSchemaKind::Any)),
TypeExpr::Str => Some(interned_scalar_schema(ScalarSchemaKind::Str)),
TypeExpr::Int => Some(interned_scalar_schema(ScalarSchemaKind::Int)),
TypeExpr::Float => Some(interned_scalar_schema(ScalarSchemaKind::Float)),
TypeExpr::Bool => Some(interned_scalar_schema(ScalarSchemaKind::Bool)),
TypeExpr::Dict => Some(interned_scalar_schema(ScalarSchemaKind::Dict)),
TypeExpr::Null => Some(interned_scalar_schema(ScalarSchemaKind::Null)),
TypeExpr::Enum(values) => {
let mut rec = record_with_capacity(2);
rec.insert(TYPE.into(), Value::String(STRING.into()));
let items: Vec<Value> = values.iter().map(|v| Value::String(v.clone())).collect();
rec.insert(ENUM.into(), Value::List(items.into()));
Some(Value::Record(Arc::new(rec)))
}
TypeExpr::List(inner) => {
let inner_value = fold_type(inner)?;
let mut rec = record_with_capacity(2);
rec.insert(TYPE.into(), Value::String(ARRAY.into()));
rec.insert(ITEMS.into(), inner_value);
Some(Value::Record(Arc::new(rec)))
}
TypeExpr::Object(fields) => {
let mut properties = record_with_capacity(fields.len());
for field in fields {
properties.insert(field.name.to_string(), fold_type(&field.ty)?);
}
let required: Vec<Value> = fields
.iter()
.filter(|f| !f.optional)
.map(|f| Value::String(f.name.clone()))
.collect();
let mut rec = record_with_capacity(4);
rec.insert(TYPE.into(), Value::String(OBJECT.into()));
rec.insert(PROPERTIES.into(), Value::Record(Arc::new(properties)));
rec.insert(REQUIRED.into(), Value::List(required.into()));
rec.insert(ADDITIONAL_PROPERTIES.into(), Value::Bool(false));
Some(Value::Record(Arc::new(rec)))
}
TypeExpr::Union(variants) => {
let folded: Option<Vec<Value>> = variants.iter().map(fold_type).collect();
let folded = folded?;
let mut rec = record_with_capacity(1);
rec.insert(ANY_OF.into(), Value::List(folded.into()));
Some(Value::Record(Arc::new(rec)))
}
TypeExpr::Process { .. } | TypeExpr::TriggerHandle(_) => {
Some(interned_scalar_schema(ScalarSchemaKind::Any))
}
TypeExpr::Ref(_) => None,
}
}
fn wrap_type_schema_value(schema: Value) -> Value {
let mut wrapper = record_with_capacity(1);
wrapper.insert(LASH_TYPE_KEY.to_string(), schema);
Value::Record(Arc::new(wrapper))
}
fn is_terminal_expr(expr: &Expr) -> bool {
match expr {
Expr::LabelAnnotated { expr, .. } => is_terminal_expr(expr),
Expr::Submit(_) | Expr::Finish(_) | Expr::Fail(_) => true,
Expr::Block(expressions) => expressions.last().is_some_and(is_terminal_expr),
Expr::If {
then_block,
else_block,
..
} => is_terminal_expr(then_block) && is_terminal_expr(else_block),
_ => false,
}
}
#[derive(Clone, Copy)]
enum ScalarSchemaKind {
Any,
Str,
Int,
Float,
Bool,
Dict,
Null,
}
fn interned_scalar_schema(kind: ScalarSchemaKind) -> Value {
static CACHE: OnceLock<[Value; 7]> = OnceLock::new();
let cache = CACHE.get_or_init(|| {
let build = |ty: &str| {
let mut rec = record_with_capacity(1);
rec.insert(schema_keys::TYPE.into(), Value::String(ty.into()));
Value::Record(Arc::new(rec))
};
[
Value::Record(Arc::new(record_with_capacity(0))), build(schema_keys::STRING),
build("integer"),
build("number"),
build("boolean"),
build(schema_keys::OBJECT),
build("null"),
]
});
cache[kind as usize].clone()
}