lashlang 0.1.0-alpha.109

Lashlang: compact CodeAct language for model-authored REPL blocks in the lash agent runtime.
Documentation
pub(crate) fn analyze_workflow_program(
    program: &Program,
    surface: &LashlangHostEnvironment,
) -> WorkflowLinkAnalysis {
    let mut linker = Linker::new(program, surface).with_expected_type_facts();
    linker.prepare_for_workflow_analysis();
    let spans = expression_spans_by_pointer(program);
    let mut analysis = WorkflowLinkAnalysis::default();

    for (index, declaration) in program.declarations.iter().enumerate() {
        let Declaration::Process(process) = declaration else {
            continue;
        };
        let mut scope = Scope::new(
            false,
            true,
            program.declaration_spans.get(index).copied(),
        );
        scope.expected_return = process.return_ty.clone();
        for param in &process.params {
            scope.bind(param.name.as_str(), linker.binding_for_type(&param.ty));
        }
        scope.bind(
            "input",
            Binding::Value(process_input_type(process)),
        );
        scope.bind(
            "inputs",
            Binding::Value(process_input_record_type(process)),
        );
        linker.analyze_workflow_block(&process.body, &mut scope, &spans, &mut analysis);
    }

    let mut main_scope = Scope::new(true, false, None);
    linker.analyze_workflow_block(
        &program.main,
        &mut main_scope,
        &spans,
        &mut analysis,
    );
    analysis
}

impl WorkflowLinkAnalysis {
    pub(crate) fn facts_for(&self, expr: &Expr) -> Option<&WorkflowLinkNodeFacts> {
        self.nodes.get(&(expr as *const Expr as usize))
    }
}

impl LinkError {
    pub fn kind(&self) -> &'static str {
        match self {
            Self::DuplicateDeclaration { .. } => "duplicate_declaration",
            Self::DuplicateProcessParam { .. } => "duplicate_process_param",
            Self::DuplicateProcessSignal { .. } => "duplicate_process_signal",
            Self::UnknownProcess { .. } => "unknown_process",
            Self::MissingProcessArgument { .. } => "missing_process_argument",
            Self::UnexpectedProcessArgument { .. } => "unexpected_process_argument",
            Self::DuplicateProcessArgument { .. } => "duplicate_process_argument",
            Self::UnknownName { .. } => "unknown_name",
            Self::UnknownBuiltin { .. } => "unknown_builtin",
            Self::UnknownResource { .. } => "unknown_resource",
            Self::UnknownType { .. } => "unknown_type",
            Self::IncompatibleConstructorInput { .. } => "incompatible_constructor_input",
            Self::IncompatibleOperationInput { .. } => "incompatible_operation_input",
            Self::IncompatibleExpectedLiteral { .. } => "incompatible_expected_literal",
            Self::IncompatibleProcessReturn { .. } => "incompatible_process_return",
            Self::InvalidTriggerRegistration { .. } => "invalid_trigger_registration",
            Self::InvalidTriggerInputs { .. } => "invalid_trigger_inputs",
            Self::DuplicateTriggerInput { .. } => "duplicate_trigger_input",
            Self::MissingTriggerInput { .. } => "missing_trigger_input",
            Self::UnknownTriggerInput { .. } => "unknown_trigger_input",
            Self::MissingTriggerEventInput { .. } => "missing_trigger_event_input",
            Self::TriggerEventOutsideInputs { .. } => "trigger_event_outside_inputs",
            Self::TriggerEventProjection { .. } => "trigger_event_projection",
            Self::InvalidTriggerList { .. } => "invalid_trigger_list",
            Self::InvalidTriggerCancel { .. } => "invalid_trigger_cancel",
            Self::UnknownTriggerEventType { .. } => "unknown_trigger_event_type",
            Self::InvalidTriggerTarget { .. } => "invalid_trigger_target",
            Self::TriggerEventMismatch { .. } => "trigger_event_mismatch",
            Self::UnresolvedReceiver { .. } => "unresolved_receiver",
            Self::UnknownResourceOperation { .. } => "unknown_resource_operation",
            Self::AmbiguousModuleOperation { .. } => "ambiguous_module_operation",
            Self::BareToolCall { .. } => "bare_tool_call",
            Self::IncompatibleProcessArgument { .. } => "incompatible_process_argument",
            Self::FeatureDisabled { .. } => "feature_disabled",
            Self::ProcessLifecycleOutsideProcess { .. } => "process_lifecycle_outside_process",
            Self::OpaqueHostDescriptorAccess { .. } => "opaque_host_descriptor_access",
            Self::UnknownObjectField { .. } => "unknown_object_field",
            Self::IncompatibleBinaryOperands { .. } => "incompatible_binary_operands",
            Self::IncompatibleIterationTarget { .. } => "incompatible_iteration_target",
            Self::ModuleHash { .. } => "module_hash",
        }
    }
}

impl<'module> Linker<'module> {
    fn prepare_for_workflow_analysis(&mut self) {
        for declaration in &self.program.declarations {
            match declaration {
                Declaration::Type(declaration) => {
                    self.type_names.insert(declaration.name.to_string());
                    self.type_defs
                        .insert(declaration.name.to_string(), declaration.ty.clone());
                }
                Declaration::Process(process) => {
                    self.process_names.insert(process.name.to_string());
                }
            }
        }
        for declaration in &self.program.declarations {
            let Declaration::Process(process) = declaration else {
                continue;
            };
            self.process_types.insert(
                process.name.to_string(),
                process_type_for_decl(
                    process,
                    process.return_ty.clone().unwrap_or(TypeExpr::Any),
                ),
            );
        }
        for (index, declaration) in self.program.declarations.iter().enumerate() {
            let Declaration::Process(process) = declaration else {
                continue;
            };
            if let Ok(output) = self.infer_process_output(
                process,
                self.program.declaration_spans.get(index).copied(),
            ) {
                self.process_types.insert(
                    process.name.to_string(),
                    process_type_for_decl(process, output),
                );
            }
        }
    }

    fn analyze_workflow_block(
        &self,
        expr: &Expr,
        scope: &mut Scope,
        spans: &BTreeMap<usize, Span>,
        analysis: &mut WorkflowLinkAnalysis,
    ) {
        match expr {
            Expr::Block(expressions) => {
                for expression in expressions {
                    self.analyze_workflow_node(expression, scope, spans, analysis);
                }
            }
            expression => self.analyze_workflow_node(expression, scope, spans, analysis),
        }
    }

    fn analyze_workflow_node(
        &self,
        expr: &Expr,
        scope: &mut Scope,
        spans: &BTreeMap<usize, Span>,
        analysis: &mut WorkflowLinkAnalysis,
    ) {
        let expression_key = expr as *const Expr as usize;
        let mut facts = WorkflowLinkNodeFacts {
            available_variables: scope
                .bindings
                .iter()
                .map(|(name, binding)| {
                    (
                        name.clone(),
                        self.resolve_type_aliases(&binding_type(Some(binding))),
                    )
                })
                .collect(),
            ..WorkflowLinkNodeFacts::default()
        };
        let before = scope.clone();
        let parent_span = scope.span;
        scope.span = spans.get(&expression_key).copied().or(parent_span);
        if let Err(error) = self.infer_expr_type(expr, scope) {
            facts.diagnostics.push(error);
            *scope = before.clone();
            recover_workflow_binding(expr, scope);
        }
        scope.span = parent_span;
        facts.expected_arguments = self.expected_arguments_for_node(expr);
        analysis.nodes.insert(expression_key, facts);
        self.analyze_nested_workflow_nodes(expr, &before, spans, analysis);
    }

    fn analyze_nested_workflow_nodes(
        &self,
        expr: &Expr,
        scope: &Scope,
        spans: &BTreeMap<usize, Span>,
        analysis: &mut WorkflowLinkAnalysis,
    ) {
        let value = workflow_node_value(expr);
        match value {
            Expr::If {
                condition,
                then_block,
                else_block,
            } => {
                let mut branch_scope = scope.clone();
                let _ = self.infer_expr_type(condition, &mut branch_scope);
                let mut then_scope = branch_scope.clone();
                self.analyze_workflow_block(then_block, &mut then_scope, spans, analysis);
                let mut else_scope = branch_scope;
                self.analyze_workflow_block(else_block, &mut else_scope, spans, analysis);
            }
            Expr::For {
                binding,
                iterable,
                body,
            } => {
                let mut body_scope = scope.clone();
                let item_ty = self
                    .infer_expr_type(iterable, &mut body_scope)
                    .and_then(|ty| self.iterable_item_type(&ty, body_scope.span))
                    .unwrap_or(TypeExpr::Any);
                body_scope.bind(binding.as_str(), self.binding_for_type(&item_ty));
                self.analyze_workflow_block(body, &mut body_scope, spans, analysis);
            }
            Expr::While { condition, body } => {
                let mut body_scope = scope.clone();
                let _ = self.infer_expr_type(condition, &mut body_scope);
                self.analyze_workflow_block(body, &mut body_scope, spans, analysis);
            }
            Expr::ListComprehension { element, clauses } => {
                let mut element_scope = scope.clone();
                for clause in clauses {
                    match clause {
                        ListComprehensionClause::For { binding, iterable } => {
                            let item_ty = self
                                .infer_expr_type(iterable, &mut element_scope)
                                .and_then(|ty| {
                                    self.iterable_item_type(&ty, element_scope.span)
                                })
                                .unwrap_or(TypeExpr::Any);
                            element_scope
                                .bind(binding.as_str(), self.binding_for_type(&item_ty));
                        }
                        ListComprehensionClause::If { condition } => {
                            let _ = self.infer_expr_type(condition, &mut element_scope);
                        }
                    }
                }
                self.analyze_workflow_block(element, &mut element_scope, spans, analysis);
            }
            _ => {}
        }
    }

    fn expected_arguments_for_node(&self, expr: &Expr) -> Vec<WorkflowLinkExpectedArgument> {
        let Some(expected_type_facts) = &self.expected_type_facts else {
            return Vec::new();
        };
        let expected_type_facts = expected_type_facts.borrow();
        let mut calls = Vec::new();
        collect_receiver_calls(workflow_node_value(expr), &mut calls);
        let multiple_calls = calls.len() > 1;
        let mut arguments = Vec::new();
        for (call_index, call) in calls.into_iter().enumerate() {
            let Expr::ReceiverCall { args, .. } = call else {
                unreachable!("receiver-call collector only returns receiver calls")
            };
            for (argument_index, argument) in args.iter().enumerate() {
                let slot = if multiple_calls {
                    format!("call[{call_index}].arg[{argument_index}]")
                } else {
                    format!("arg[{argument_index}]")
                };
                collect_expected_slots(
                    argument,
                    slot,
                    &expected_type_facts.by_expression,
                    &mut arguments,
                );
            }
        }
        arguments
    }
}

fn workflow_node_value(mut expr: &Expr) -> &Expr {
    while let Expr::LabelAnnotated { expr: inner, .. } = expr {
        expr = inner;
    }
    if let Expr::Assign { expr: value, .. } = expr {
        value
    } else {
        expr
    }
}

fn recover_workflow_binding(expr: &Expr, scope: &mut Scope) {
    let mut expr = expr;
    while let Expr::LabelAnnotated { expr: inner, .. } = expr {
        expr = inner;
    }
    if let Expr::Assign { target, .. } = expr
        && target.steps.is_empty()
    {
        scope.bind(target.root.as_str(), any_binding());
    }
}

fn collect_receiver_calls<'a>(expr: &'a Expr, calls: &mut Vec<&'a Expr>) {
    if matches!(expr, Expr::ReceiverCall { .. }) {
        calls.push(expr);
    }
    for child in expr.children() {
        collect_receiver_calls(child, calls);
    }
}

fn collect_expected_slots(
    expr: &Expr,
    slot: String,
    expected: &BTreeMap<usize, TypeExpr>,
    arguments: &mut Vec<WorkflowLinkExpectedArgument>,
) {
    if let Some(ty) = expected.get(&(expr as *const Expr as usize)) {
        arguments.push(WorkflowLinkExpectedArgument {
            slot: slot.clone(),
            ty: ty.clone(),
        });
    }
    match expr {
        Expr::LabelAnnotated { expr, .. }
        | Expr::Await(expr)
        | Expr::ResultUnwrap(expr) => collect_expected_slots(expr, slot, expected, arguments),
        Expr::Record(entries) => {
            for (name, value) in entries {
                collect_expected_slots(
                    value,
                    format!("{slot}.{}", name.as_str()),
                    expected,
                    arguments,
                );
            }
        }
        Expr::List(items) | Expr::Tuple(items) => {
            for (index, item) in items.iter().enumerate() {
                collect_expected_slots(
                    item,
                    format!("{slot}[{index}]"),
                    expected,
                    arguments,
                );
            }
        }
        _ => {}
    }
}

fn expression_spans_by_pointer(program: &Program) -> BTreeMap<usize, Span> {
    let spans_by_path = program
        .expression_source_spans
        .iter()
        .map(|source_span| (source_span.path.clone(), source_span.span))
        .collect::<BTreeMap<_, _>>();
    let mut spans = BTreeMap::new();
    collect_expression_spans_by_pointer(
        &program.main,
        &mut Vec::new(),
        &spans_by_path,
        &mut spans,
    );
    spans
}

fn collect_expression_spans_by_pointer(
    expr: &Expr,
    path: &mut Vec<u32>,
    spans_by_path: &BTreeMap<Vec<u32>, Span>,
    spans: &mut BTreeMap<usize, Span>,
) {
    if let Some(span) = spans_by_path.get(path.as_slice()).copied() {
        spans.insert(expr as *const Expr as usize, span);
    }
    for (index, child) in expr.children().enumerate() {
        path.push(index.try_into().expect("AST child index fits u32"));
        collect_expression_spans_by_pointer(child, path, spans_by_path, spans);
        path.pop();
    }
}