use super::*;
pub(crate) fn lower_program(
program: Program,
workflow_inputs: BTreeMap<String, WorkflowInputSurface>,
shared_coordination_usage: Vec<IrSharedCoordinationUsage>,
) -> CompileOutput {
let mut diagnostics = Vec::new();
let mut warnings = Vec::new();
let (program, pattern_applications) = expand_pattern_applications(program, &mut diagnostics);
let pending_regions: BTreeMap<String, IrRegion>;
let program = {
let mut program = program;
let actions: Vec<ActionDecl> = program
.items
.iter()
.filter_map(|item| match item {
Item::Action(action) => Some(action.clone()),
_ => None,
})
.collect();
let mut expanded = Vec::with_capacity(program.items.len());
for item in program.items {
match item {
Item::Action(_) => {}
other => expanded.push(other),
}
}
for item in &mut expanded {
if let Item::Rule(rule) = item {
if rule.body.text.contains('#') {
rule.body.text = body::blank_full_line_comments(&rule.body.text);
}
}
}
action_expand::expand_action_calls(&mut expanded, &actions, &mut diagnostics);
then_expand::expand_then_statements(&mut expanded, &mut diagnostics);
pending_regions = extract_rule_regions(&mut expanded, &mut diagnostics);
program.items = expanded;
program
};
let schema_names = collect_schema_names(&program, &mut diagnostics);
let harness_kinds = collect_harness_kinds(&program, &mut diagnostics);
let agent_names = collect_agent_names(&program, &mut diagnostics);
let workflow_contract_names = collect_workflow_contract_names(&program, &mut diagnostics);
let mut semantic = SemanticContext::from_program(&program, workflow_inputs);
semantic.regions = pending_regions.clone();
let workflow = match program.workflow {
Some(workflow) => workflow.name,
None => {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: SourceSpan { start: 0, end: 0 },
message: "expected workflow declaration".to_owned(),
suggestion: Some("add `workflow Name` before declarations".to_owned()),
});
"<missing>".to_owned()
}
};
let mut ir = IrProgram {
workflow,
source_tags: Vec::new(),
source_descriptions: Vec::new(),
includes: Vec::new(),
pattern_applications,
workflow_contracts: Vec::new(),
uses: Vec::new(),
harnesses: Vec::new(),
trackers: Vec::new(),
streams: Vec::new(),
channels: Vec::new(),
credentials: Vec::new(),
gauges: Vec::new(),
marks: Vec::new(),
campaigns: Vec::new(),
file_stores: Vec::new(),
memory_pools: Vec::new(),
events: Vec::new(),
sources: Vec::new(),
tests: Vec::new(),
leases: Vec::new(),
ledgers: Vec::new(),
counters: Vec::new(),
shared_coordination_usage,
schemas: Vec::new(),
agents: Vec::new(),
coerces: Vec::new(),
assertions: Vec::new(),
rules: Vec::new(),
rule_dependencies: Vec::new(),
};
let workflow_tag_target = ir.workflow.clone();
lower_source_tags(
&program.workflow_tags,
"workflow",
&workflow_tag_target,
&mut ir,
);
lower_source_description(
program.workflow_description.as_ref(),
"workflow",
&workflow_tag_target,
&mut ir,
);
collect_inline_decide_schemas(&program.items, &mut semantic, &mut ir);
collect_redact_schemas(&program.items, &mut semantic, &mut ir);
for item in program.items {
match item {
Item::Include(include) => lower_include(include, &mut ir),
Item::WorkflowContract(contract) => lower_workflow_contract(
contract,
&mut ir,
&schema_names,
&agent_names,
&mut diagnostics,
),
Item::Use(use_decl) => lower_use(use_decl, &mut ir, &mut diagnostics),
Item::Action(action) => {
let _ = action;
}
Item::Pattern(pattern) => diagnostics.push(Diagnostic {
related: Vec::new(),
span: pattern.span,
message: format!(
"pattern `{}` is not allowed inside this declaration scope",
pattern.name.name
),
suggestion: Some("declare patterns at source top level".to_owned()),
}),
Item::Apply(apply) => diagnostics.push(Diagnostic {
related: Vec::new(),
span: apply.span,
message: format!(
"pattern application `{}` was not expanded",
apply.alias.name
),
suggestion: Some(
"ensure the applied pattern is declared at source top level".to_owned(),
),
}),
Item::Harness(harness) => lower_harness(harness, &mut ir, &mut diagnostics),
Item::Tracker(queue) => lower_tracker(queue, &mut ir, &mut diagnostics),
Item::Channel(channel) => lower_channel(channel, &mut ir, &mut diagnostics),
Item::Credential(credential) => lower_credential(credential, &mut ir, &mut diagnostics),
Item::Stream(stream) => lower_stream(stream, &mut ir, &mut diagnostics),
Item::Gauge(gauge) => lower_gauge(gauge, &mut ir, &mut diagnostics),
Item::Mark(mark) => lower_mark(mark, &mut ir, &mut diagnostics),
Item::Campaign(campaign) => lower_campaign(campaign, &mut ir, &mut diagnostics),
Item::FileStore(file_store) => {
if let Some(provider) = &file_store.provider {
if !FILE_STORE_PROVIDERS.contains(&provider.name.as_str()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: provider.span,
message: format!(
"file store `{}` names unknown provider `{}`",
file_store.name.name, provider.name
),
suggestion: Some(format!(
"declare one of the v1 file providers: {}",
FILE_STORE_PROVIDERS.join(", ")
)),
});
}
}
ir.file_stores.push(IrFileStore {
name: file_store.name.name,
root: file_store.root,
read_globs: file_store.read_globs,
write_globs: file_store.write_globs,
provider: file_store.provider.map(|provider| provider.name),
});
}
Item::MemoryPool(pool) => {
ir.memory_pools.push(IrMemoryPool {
name: pool.name.name,
context_limit: pool.context_limit,
});
}
Item::Agent(agent) => lower_agent(agent, &mut ir, &harness_kinds, &mut diagnostics),
Item::Enum(enum_decl) => lower_enum(enum_decl, &mut ir, &mut diagnostics),
Item::Event(event) => lower_event(event, &mut ir, &mut diagnostics),
Item::Source(source) => {
validate_source_emit_signal_declared(
&source,
&semantic.schemas.events,
&mut diagnostics,
);
lower_source(*source, &mut ir, &mut diagnostics)
}
Item::Test(test) => lower_test(test, &mut ir, &mut diagnostics),
Item::Lease(lease) => {
if !schema_names.contains(&lease.key_type.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: lease.key_type.span,
message: format!(
"lease `{}` keys on undeclared type `{}`",
lease.name.name, lease.key_type.name
),
suggestion: Some(
"key a lease on an entity class the workflow already models".to_owned(),
),
});
}
ir.leases.push(IrLease {
name: lease.name.name,
key_type: lease.key_type.name,
slots: lease.slots.max(1),
ttl_seconds: lease.ttl_seconds,
shared: lease.shared,
span: lease.span,
});
}
Item::Ledger(ledger) => {
if !schema_names.contains(&ledger.entry_schema.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: ledger.entry_schema.span,
message: format!(
"ledger `{}` records undeclared entry type `{}`",
ledger.name.name, ledger.entry_schema.name
),
suggestion: Some("declare the entry class before the ledger".to_owned()),
});
}
ir.ledgers.push(IrLedger {
name: ledger.name.name,
entry_schema: ledger.entry_schema.name,
partition_field: ledger.partition_field.name,
retain_seconds: ledger.retain_seconds,
shared: ledger.shared,
span: ledger.span,
});
}
Item::Counter(counter) => {
if !schema_names.contains(&counter.key_type.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: counter.key_type.span,
message: format!(
"counter `{}` keys on undeclared type `{}`",
counter.name.name, counter.key_type.name
),
suggestion: Some(
"key a counter on an entity class the workflow already models"
.to_owned(),
),
});
}
ir.counters.push(IrCounter {
name: counter.name.name,
key_type: counter.key_type.name,
cap: counter.cap,
reset: counter.reset,
timezone: counter.timezone,
shared: counter.shared,
span: counter.span,
});
}
Item::Class(class_decl) => lower_class(
class_decl,
&mut ir,
&schema_names,
&agent_names,
&mut diagnostics,
),
Item::Table(table) => {
lower_source_tags(&table.tags, "table", &table.name.name, &mut ir);
lower_source_description(
table.description.as_ref(),
"table",
&table.name.name,
&mut ir,
);
lower_table(
table,
&semantic,
&workflow_contract_names,
&mut ir,
&mut diagnostics,
)
}
Item::Coerce(coerce) => lower_coerce(
coerce,
&mut ir,
&schema_names,
&agent_names,
&mut diagnostics,
),
Item::Assert(assertion) => {
let assertion_target = stable_hash(&assertion.expr);
lower_source_tags(&assertion.tags, "assertion", &assertion_target, &mut ir);
lower_source_description(
assertion.description.as_ref(),
"assertion",
&assertion_target,
&mut ir,
);
lower_assert(assertion, &semantic, &mut ir, &mut diagnostics)
}
Item::Rule(rule) => {
lower_source_tags(&rule.tags, "rule", &rule.name.name, &mut ir);
lower_source_description(
rule.description.as_ref(),
"rule",
&rule.name.name,
&mut ir,
);
lower_rule(
rule,
&semantic,
&workflow_contract_names,
&mut ir,
&mut diagnostics,
)
}
}
}
validate_streams(&ir, &mut diagnostics);
ir.rule_dependencies = build_rule_dependencies(&ir.rules);
validate_turn_access_grant_file_operations(&ir, &mut diagnostics);
validate_turn_access_grant_memory_operations(&ir, &mut diagnostics);
for rule in &mut ir.rules {
if let Some(region) = pending_regions.get(&rule.name) {
rule.metadata.region = Some(region.clone());
}
}
expand_source_emit_from(&mut ir, &mut diagnostics);
validate_file_store_write_policy(&ir, &mut diagnostics);
warn_inert_memory_grant_on_native_adapter(&ir, &mut warnings);
warn_counter_without_timezone(&ir, &mut warnings);
warn_unhandled_effect_failures(&ir, &mut warnings);
validate_improve_declarations(&ir, &mut diagnostics);
CompileOutput {
ir: diagnostics.is_empty().then_some(ir),
diagnostics,
warnings,
}
}
fn lower_source_tags(tags: &[TagDecl], target_kind: &str, target: &str, ir: &mut IrProgram) {
for tag in tags {
ir.source_tags.push(IrSourceTag {
name: tag.name.clone(),
target_kind: target_kind.to_owned(),
target: target.to_owned(),
span: tag.span,
});
}
}
fn lower_source_description(
description: Option<&StringLiteral>,
target_kind: &str,
target: &str,
ir: &mut IrProgram,
) {
if let Some(description) = description {
ir.source_descriptions.push(IrSourceDescription {
value: description.value.clone(),
target_kind: target_kind.to_owned(),
target: target.to_owned(),
span: description.span,
});
}
}
fn lower_assert(
assertion: AssertDecl,
semantic: &SemanticContext,
ir: &mut IrProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
match parse_expression(&assertion.expr) {
Ok(expr) => {
validate_parsed_expression(
&expr,
semantic,
&ExprScope::default(),
&ExprValidationContext::assertion(assertion.span),
"assertion",
diagnostics,
);
let mut projection_reads = collect_projection_reads(&expr);
sort_projection_reads(&mut projection_reads);
ir.assertions.push(IrAssertion {
expr: IrExpression {
source: assertion.expr,
expr,
span: assertion.span,
},
projection_reads,
});
}
Err(message) => diagnostics.push(Diagnostic {
related: Vec::new(),
span: assertion.span,
message: format!("invalid assertion expression: {message}"),
suggestion: Some(
"use a deterministic expression such as `count(Fact) == 1`".to_owned(),
),
}),
}
}
pub(crate) fn lower_expression(source: &str, span: SourceSpan) -> Option<IrExpression> {
parse_expression(source).ok().map(|expr| IrExpression {
source: source.to_owned(),
expr,
span,
})
}
fn lower_include(include: IncludeDecl, ir: &mut IrProgram) {
ir.includes.push(IrInclude {
path: include.path.value,
source_hash: None,
});
}
fn lower_workflow_contract(
contract: WorkflowContractDecl,
ir: &mut IrProgram,
schema_names: &BTreeSet<String>,
agent_names: &BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
validate_type_refs(&contract.ty, schema_names, agent_names, diagnostics);
let kind = match contract.kind {
WorkflowContractKind::Input => IrWorkflowContractKind::Input,
WorkflowContractKind::Output => IrWorkflowContractKind::Output,
WorkflowContractKind::Failure => IrWorkflowContractKind::Failure,
};
ir.workflow_contracts.push(IrWorkflowContract {
kind,
name: contract.name.name,
ty: lower_type(contract.ty),
span: contract.span,
});
}
fn lower_use(use_decl: UseDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
let std_family_known = |value: &str| {
STD_PACKAGE_IDS.iter().any(|id| {
value == *id
|| value
.strip_prefix(id)
.is_some_and(|rest| rest.starts_with('.'))
})
};
if use_decl.name.value.starts_with("std.") && !std_family_known(&use_decl.name.value) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: use_decl.name.span,
message: format!("unknown standard package `{}`", use_decl.name.value),
suggestion: Some(format!(
"standard packages are {}",
STD_PACKAGE_IDS.join(", ")
)),
});
}
let kind = IrUseKind::Package;
ir.uses.push(IrUse {
kind,
name: use_decl.name.value,
});
}
fn lower_tracker(tracker: TrackerDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if tracker.provider.name != "builtin" {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: tracker.provider.span,
message: format!(
"tracker `{}` uses unavailable provider `{}`",
tracker.name.name, tracker.provider.name
),
suggestion: Some(
"`builtin` is the available provider; github/linear/jira are deferred bindings"
.to_owned(),
),
});
}
ir.trackers.push(IrTracker {
name: tracker.name.name,
provider: tracker.provider.name,
span: tracker.span,
});
}
fn lower_stream(stream: StreamDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if let Some(existing) = ir
.streams
.iter()
.find(|other| other.name == stream.name.name)
{
diagnostics.push(Diagnostic {
related: Vec::new(),
span: stream.name.span,
message: format!("duplicate stream `{}`", stream.name.name),
suggestion: Some(format!(
"stream `{}` is already declared with members [{}]",
existing.name,
existing.members.join(", ")
)),
});
return;
}
ir.streams.push(IrStream {
name: stream.name.name,
members: stream
.members
.iter()
.map(|member| member.name.clone())
.collect(),
member_spans: stream.members.iter().map(|member| member.span).collect(),
staleness_seconds: stream.staleness_seconds,
span: stream.span,
});
}
fn lower_channel(channel: ChannelDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if let Some(existing) = ir
.channels
.iter()
.find(|other| other.name == channel.name.name)
{
diagnostics.push(
Diagnostic {
related: Vec::new(),
span: channel.name.span,
message: format!("channel `{}` is declared more than once", channel.name.name),
suggestion: Some("give each channel a unique name".to_owned()),
}
.with_related(existing.span, "first declared here"),
);
return;
}
if channel_provider_report(&channel.provider.name).is_none() {
let known = CHANNEL_PROVIDER_REPORTS
.iter()
.map(|report| report.short_name)
.collect::<Vec<_>>()
.join(", ");
diagnostics.push(Diagnostic {
related: Vec::new(),
span: channel.provider.span,
message: format!(
"channel `{}` names unknown messaging provider `{}`",
channel.name.name, channel.provider.name
),
suggestion: Some(format!("declare one of the v1 providers: {known}")),
});
}
ir.channels.push(IrChannel {
name: channel.name.name,
provider: channel.provider.name,
workspace: channel.workspace.map(|workspace| workspace.name),
destination: channel.destination.map(|destination| destination.value),
span: channel.span,
});
}
fn lower_credential(
credential: CredentialDecl,
ir: &mut IrProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
if let Some(existing) = ir
.credentials
.iter()
.find(|declared| declared.name == credential.name.name)
{
diagnostics.push(
Diagnostic {
related: Vec::new(),
span: credential.span,
message: format!(
"credential `{}` is declared more than once",
credential.name.name
),
suggestion: Some("give each credential a unique name".to_owned()),
}
.with_related(existing.span, "first declared here"),
);
return;
}
let normalized = credential.kind.name.replace('_', "-");
if whipplescript_custody::CredentialKind::parse(&normalized).is_err() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: credential.kind.span,
message: format!(
"credential `{}` names unknown kind `{}`",
credential.name.name, credential.kind.name
),
suggestion: Some(
"declare one of: bearer, basic, raw, hmac_sha256, ed25519, aws_sigv4, jwt_rs256"
.to_owned(),
),
});
}
ir.credentials.push(IrCredential {
name: credential.name.name,
kind: normalized,
span: credential.span,
});
}
fn lower_gauge(gauge: GaugeDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if let Some(existing) = ir.gauges.iter().find(|other| other.name == gauge.name.name) {
diagnostics.push(
Diagnostic {
related: Vec::new(),
span: gauge.name.span,
message: format!("gauge `{}` is declared more than once", gauge.name.name),
suggestion: Some("give each gauge a unique name".to_owned()),
}
.with_related(existing.span, "first declared here"),
);
return;
}
let (judge_kind, judge_target, judge_args) = match &gauge.judge {
GaugeJudge::Coerce(target, args) => ("coerce", target.name.clone(), args.clone()),
GaugeJudge::Prompt(template) => ("prompt", template.value.clone(), Vec::new()),
GaugeJudge::Exec(command) => ("exec", command.value.clone(), Vec::new()),
GaugeJudge::Labels(source) => ("labels", source.value.clone(), Vec::new()),
};
let expect = gauge.expect.as_ref().map(|bar| IrGaugeBar {
form: match &bar.subject {
GaugeBarSubject::Chance { .. } => "chance".to_owned(),
GaugeBarSubject::Stat { .. } => "stat".to_owned(),
},
subject: match &bar.subject {
GaugeBarSubject::Chance { field } => field.name.clone(),
GaugeBarSubject::Stat { stat } => stat.name.clone(),
},
op: if bar.at_least { ">=" } else { "<=" }.to_owned(),
threshold: bar.threshold.clone(),
});
ir.gauges.push(IrGauge {
name: gauge.name.name,
site: gauge.site,
judge_kind: judge_kind.to_owned(),
judge_target,
judge_args,
expect,
inputs: gauge.inputs.into_iter().map(|input| input.name).collect(),
span: gauge.span,
});
}
fn lower_mark(mark: MarkDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if let Some(existing) = ir.marks.iter().find(|other| other.name == mark.name.value) {
diagnostics.push(
Diagnostic {
related: Vec::new(),
span: mark.name.span,
message: format!("mark `{}` is declared more than once", mark.name.value),
suggestion: Some("give each mark a unique name".to_owned()),
}
.with_related(existing.span, "first declared here"),
);
return;
}
ir.marks.push(IrMark {
name: mark.name.value,
site: mark.site,
span: mark.span,
});
}
fn lower_campaign(campaign: CampaignDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if let Some(existing) = ir
.campaigns
.iter()
.find(|other| other.name == campaign.name.name)
{
diagnostics.push(
Diagnostic {
related: Vec::new(),
span: campaign.name.span,
message: format!(
"campaign `{}` is declared more than once",
campaign.name.name
),
suggestion: Some("give each campaign a unique name".to_owned()),
}
.with_related(existing.span, "first declared here"),
);
return;
}
ir.campaigns.push(IrCampaign {
name: campaign.name.name,
ascend: campaign
.ascend
.into_iter()
.map(|gauge| gauge.name)
.collect(),
reach: campaign
.reach
.into_iter()
.map(|reach| IrCampaignReach {
gauge: reach.gauge.name,
op: if reach.at_least { ">=" } else { "<=" }.to_owned(),
threshold: reach.threshold,
unit: reach.unit,
})
.collect(),
guard: campaign
.guard
.into_iter()
.map(|guard| IrCampaignGuard {
gauge: guard.gauge.name,
band_percent: guard.band_percent,
})
.collect(),
sacrifice: campaign
.sacrifice
.into_iter()
.map(|gauge| gauge.name)
.collect(),
proposer_redacted: campaign.proposer_redacted,
span: campaign.span,
});
}
fn lower_harness(harness: HarnessDecl, ir: &mut IrProgram, _diagnostics: &mut [Diagnostic]) {
ir.harnesses.push(IrHarness {
name: harness.name.name,
kind: harness.kind.name,
span: harness.span,
});
}
fn lower_agent(
agent: AgentDecl,
ir: &mut IrProgram,
harness_kinds: &BTreeMap<String, String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut lowered = IrAgent {
name: agent.name.name.clone(),
span: agent.name.span,
harness: agent.harness.as_ref().map(|harness| harness.name.clone()),
provider: None,
profile: None,
capacity: None,
skills: Vec::new(),
capabilities: Vec::new(),
requires: Vec::new(),
tools: Vec::new(),
compaction: None,
thread: None,
settings: None,
harness_class: HarnessClass::Managed,
};
if let Some(harness) = &agent.harness {
if !harness_kinds.contains_key(&harness.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: harness.span,
message: format!(
"agent `{}` uses unknown harness `{}`",
agent.name.name, harness.name
),
suggestion: Some(format!(
"declare `harness {}: fixture` before using it",
harness.name
)),
});
}
}
if let Some(delegate) = &agent.delegated_to {
if harness_class(&delegate.name) != HarnessClass::Delegated {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: delegate.span,
message: format!(
"agent `{}` delegates to `{}`, which is a managed kind",
agent.name.name, delegate.name
),
suggestion: Some(
"a plain `agent name { ... }` is managed by default; `delegated to` names a foreign runtime"
.to_owned(),
),
});
}
}
let mut compaction_span: Option<SourceSpan> = None;
let mut thread_span: Option<SourceSpan> = None;
let mut settings_span: Option<SourceSpan> = None;
for field in agent.fields {
match field {
AgentField::Provider(provider) => {
if lowered.provider.is_some() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: provider.span,
message: format!(
"agent `{}` declares provider more than once",
agent.name.name
),
suggestion: Some(
"keep exactly one `provider` field in the agent block".to_owned(),
),
});
}
if agent.harness.is_some() {
diagnostics.push(Diagnostic { related: Vec::new(),
span: provider.span,
message: format!(
"agent `{}` declares both `using` harness and direct provider `{}`",
agent.name.name, provider.name
),
suggestion: Some(
"use either `agent name using harness { ... }` or `provider codex`, not both"
.to_owned(),
),
});
}
if agent.delegated_to.is_some() {
diagnostics.push(Diagnostic { related: Vec::new(),
span: provider.span,
message: format!(
"agent `{}` declares both `delegated to` and direct provider `{}`",
agent.name.name, provider.name
),
suggestion: Some(
"use either `agent name delegated to <provider> { ... }` or a `provider` field, not both"
.to_owned(),
),
});
}
lowered.provider = Some(provider.name);
}
AgentField::Profile(profile) => lowered.profile = Some(profile.value),
AgentField::Capacity(capacity, span) => {
if capacity == 0 {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"agent `{}` capacity must be greater than zero",
agent.name.name
),
suggestion: Some("use `capacity 1` or a larger integer".to_owned()),
});
}
lowered.capacity = Some(capacity);
}
AgentField::Skills(skills, _) => {
let mut seen = BTreeSet::new();
for skill in skills {
if !seen.insert(skill.value.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: skill.span,
message: format!(
"agent `{}` attaches skill `{}` more than once",
agent.name.name, skill.value
),
suggestion: Some("remove the duplicate skill entry".to_owned()),
});
}
lowered.skills.push(skill.value);
}
}
AgentField::Capabilities(capabilities, _) => {
let mut seen = BTreeSet::new();
for capability in capabilities {
if !seen.insert(capability.value.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: capability.span,
message: format!(
"agent `{}` declares capability `{}` more than once",
agent.name.name, capability.value
),
suggestion: Some("remove the duplicate capability entry".to_owned()),
});
}
lowered.capabilities.push(capability.value);
}
}
AgentField::Requires(classes, _) => {
let mut seen = BTreeSet::new();
for class in classes {
if !whipplescript_core::AGENT_FEATURE_CLASS_TAXONOMY
.contains(&class.name.as_str())
{
diagnostics.push(Diagnostic {
related: Vec::new(),
span: class.span,
message: format!(
"agent `{}` requires unknown feature class `{}`",
agent.name.name, class.name
),
suggestion: Some(format!(
"feature classes come from the DR-0015 taxonomy: {}",
whipplescript_core::AGENT_FEATURE_CLASS_TAXONOMY.join(", ")
)),
});
}
if !seen.insert(class.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: class.span,
message: format!(
"agent `{}` requires feature class `{}` more than once",
agent.name.name, class.name
),
suggestion: Some("remove the duplicate requires entry".to_owned()),
});
}
lowered.requires.push(class.name);
}
}
AgentField::Tools(tools, _) => {
let mut seen = BTreeSet::new();
for tool in tools {
if !seen.insert(tool.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: tool.span,
message: format!(
"agent `{}` grants tool `{}` more than once",
agent.name.name, tool.name
),
suggestion: Some("remove the duplicate tool entry".to_owned()),
});
}
lowered.tools.push(tool.name);
}
}
AgentField::Compaction(strategy) => {
const STRATEGIES: [&str; 4] = ["summarize", "hard_reset", "tool_results", "none"];
if lowered.compaction.is_some() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: strategy.span,
message: format!(
"agent `{}` declares compaction more than once",
agent.name.name
),
suggestion: Some("keep exactly one `compaction` field".to_owned()),
});
}
if !STRATEGIES.contains(&strategy.name.as_str()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: strategy.span,
message: format!(
"agent `{}` uses unknown compaction strategy `{}`",
agent.name.name, strategy.name
),
suggestion: Some(
"supported strategies are `summarize`, `hard_reset`, `tool_results`, and `none`"
.to_owned(),
),
});
}
compaction_span = Some(strategy.span);
lowered.compaction = Some(strategy.name);
}
AgentField::Thread(mode) => {
const MODES: [&str; 2] = ["continue", "fresh"];
if lowered.thread.is_some() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: mode.span,
message: format!(
"agent `{}` declares thread more than once",
agent.name.name
),
suggestion: Some("keep exactly one `thread` field".to_owned()),
});
}
if !MODES.contains(&mode.name.as_str()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: mode.span,
message: format!(
"agent `{}` uses unknown thread mode `{}`",
agent.name.name, mode.name
),
suggestion: Some(
"supported thread modes are `continue` and `fresh`".to_owned(),
),
});
}
thread_span = Some(mode.span);
lowered.thread = Some(mode.name);
}
AgentField::Settings(sources) => {
const SOURCES: [&str; 3] = ["project", "user", "none"];
if lowered.settings.is_some() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: sources.span,
message: format!(
"agent `{}` declares settings more than once",
agent.name.name
),
suggestion: Some("keep exactly one `settings` field".to_owned()),
});
}
if !SOURCES.contains(&sources.name.as_str()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: sources.span,
message: format!(
"agent `{}` uses unknown settings source `{}`",
agent.name.name, sources.name
),
suggestion: Some(
"supported settings sources are `project`, `user`, and `none`"
.to_owned(),
),
});
}
settings_span = Some(sources.span);
lowered.settings = Some(sources.name);
}
AgentField::Unknown { name, .. } => {
diagnostics.push(Diagnostic { related: Vec::new(),
span: name.span,
message: format!(
"unknown agent field `{}` on agent `{}`",
name.name, agent.name.name
),
suggestion: Some(
"supported agent fields are `provider`, `profile`, `capacity`, `skills`, `capabilities`, `tools`, `compaction`, and `settings`".to_owned(),
),
});
}
}
}
if lowered.provider.is_none() {
if let Some(delegate) = &agent.delegated_to {
lowered.provider = Some(delegate.name.clone());
} else if lowered.harness.is_none() {
lowered.provider = Some("owned".to_owned());
}
}
let resolved_kind = lowered.provider.as_deref().or_else(|| {
lowered
.harness
.as_deref()
.and_then(|name| harness_kinds.get(name).map(String::as_str))
});
lowered.harness_class = resolved_kind
.map(harness_class)
.unwrap_or(HarnessClass::Managed);
if resolved_kind.is_some() {
if lowered.harness_class == HarnessClass::Delegated {
if let Some(span) = compaction_span {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"agent `{}` is delegated; `compaction` is a managed-harness knob",
agent.name.name
),
suggestion: Some(
"remove `compaction` — a delegated harness compacts its own context"
.to_owned(),
),
});
}
if let Some(span) = thread_span {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"agent `{}` is delegated; `thread` is a managed-harness knob",
agent.name.name
),
suggestion: Some(
"remove `thread` — a delegated harness owns its own conversation state"
.to_owned(),
),
});
}
} else if let Some(span) = settings_span {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"agent `{}` is managed; `settings` is a delegated-harness knob",
agent.name.name
),
suggestion: Some(
"remove `settings` — WhippleScript assembles a managed agent's context"
.to_owned(),
),
});
}
}
if lowered.profile.is_none() {
lowered.profile = Some("no-repo".to_owned());
}
if lowered.capacity.is_none() {
lowered.capacity = Some(1);
}
ir.agents.push(lowered);
}
fn lower_enum(enum_decl: EnumDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
let mut variants = BTreeSet::new();
for variant in &enum_decl.variants {
if !variants.insert(variant.name.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: variant.span,
message: format!(
"enum `{}` declares variant `{}` more than once",
enum_decl.name.name, variant.name.name
),
suggestion: Some(
"remove the duplicate variant or give it a distinct name".to_owned(),
),
});
}
for field in &variant.fields {
if field.name.name == "variant" {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: field.name.span,
message: format!(
"variant `{}` of enum `{}` declares reserved field `variant`",
variant.name.name, enum_decl.name.name
),
suggestion: Some(
"the discriminant is synthesized from the variant name; rename the field"
.to_owned(),
),
});
}
}
}
for variant in &enum_decl.variants {
if variant.fields.is_empty() {
continue;
}
let mut fields = vec![IrClassField {
name: "variant".to_owned(),
ty: IrType::LiteralString(variant.name.name.clone()),
is_key: false,
presence_condition: None,
span: variant.name.span,
}];
fields.extend(variant.fields.iter().map(|field| IrClassField {
name: field.name.name.clone(),
ty: lower_type(field.ty.clone()),
is_key: false,
presence_condition: field.presence_condition.clone(),
span: field.span,
}));
ir.schemas.push(IrSchema::Class(IrClass {
name: format!("{}.{}", enum_decl.name.name, variant.name.name),
fields,
span: variant.span,
}));
}
ir.schemas.push(IrSchema::Enum(IrEnum {
name: enum_decl.name.name,
variants: enum_decl
.variants
.into_iter()
.map(|variant| variant.name.name)
.collect(),
span: enum_decl.span,
}));
}
fn lower_test(test: TestDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if ir
.tests
.iter()
.any(|existing| existing.name == test.name.value)
{
diagnostics.push(Diagnostic {
related: Vec::new(),
span: test.name.span,
message: format!("test `{}` is declared more than once", test.name.value),
suggestion: Some("give each test scenario a distinct name".to_owned()),
});
}
if !test
.clauses
.iter()
.any(|clause| matches!(clause, TestClause::Expect(_)))
{
diagnostics.push(Diagnostic {
related: Vec::new(),
span: test.span,
message: format!("test `{}` has no `expect` clause", test.name.value),
suggestion: Some("a test must assert at least one expected outcome".to_owned()),
});
}
for clause in &test.clauses {
match clause {
TestClause::Given(
GivenClause::Input { fields, .. }
| GivenClause::Fact { fields, .. }
| GivenClause::Signal { fields, .. },
) => {
for field in fields {
validate_test_expr_source(
&format!("given field `{}`", field.name.name),
&field.value,
field.span,
diagnostics,
);
}
}
TestClause::Expect(ExpectClause {
target: ExpectTarget::Projection(query),
..
}) => match &query.kind {
ProjQueryKind::Count { predicate, .. } | ProjQueryKind::Where { predicate } => {
validate_test_expr_source(
&format!("predicate on `{}`", query.noun),
predicate,
query.span,
diagnostics,
);
}
ProjQueryKind::Exists => {}
},
_ => {}
}
}
ir.tests.push(IrTest {
name: test.name.value,
workflow: test.workflow.map(|identifier| identifier.name),
clauses: test.clauses,
span: test.span,
});
}
fn lower_source(source: SourceDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if ir
.sources
.iter()
.any(|existing| existing.name == source.name.name)
{
diagnostics.push(Diagnostic {
related: Vec::new(),
span: source.name.span,
message: format!("source `{}` is declared more than once", source.name.name),
suggestion: Some("remove the duplicate source declaration".to_owned()),
});
}
if let Some(clock) = &source.clock {
let recurring = !matches!(clock.recurrence, Recurrence::At { .. });
if recurring && clock.missed.is_none() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: clock.span,
message: format!(
"recurring source `{}` must declare a `missed` policy",
source.name.name
),
suggestion: Some(
"add `missed skip`, `missed coalesce`, or `missed catch_up limit N`".to_owned(),
),
});
}
if matches!(clock.recurrence, Recurrence::EveryCalendar { .. }) && clock.timezone.is_none()
{
diagnostics.push(Diagnostic { related: Vec::new(),
span: clock.span,
message: format!(
"calendar source `{}` should declare a `timezone`",
source.name.name
),
suggestion: Some(
"add `timezone \"America/New_York\"`; a calendar schedule without one defaults to UTC".to_owned(),
),
});
}
}
let is_file = source.provider.name == "file";
if is_file && source.path.is_none() && source.watch.is_none() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: source.span,
message: format!(
"`file` source `{}` requires a `path` or `watch` clause",
source.name.name
),
suggestion: Some(
"add `path \"./inbox.txt\"` (one signal per line) or `watch \"./drops/*.json\"` \
(one signal per new file-content occurrence)"
.to_owned(),
),
});
}
if is_file && source.path.is_some() && source.watch.is_some() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: source
.watch
.as_ref()
.map(|watch| watch.span)
.unwrap_or(source.span),
message: format!(
"`file` source `{}` declares both `path` and `watch`; the modes are exclusive",
source.name.name
),
suggestion: Some(
"keep `path` for line-by-line admission or `watch` for per-file-content \
occurrences, not both"
.to_owned(),
),
});
}
if !is_file {
if let Some(path) = &source.path {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: path.span,
message: format!(
"source `{}` declares a `path` clause but its provider is `{}`, not `file`",
source.name.name, source.provider.name
),
suggestion: Some(
"use `source file as ...` for a `path`, or remove the clause".to_owned(),
),
});
}
if let Some(watch) = &source.watch {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: watch.span,
message: format!(
"source `{}` declares a `watch` clause but its provider is `{}`, not `file`",
source.name.name, source.provider.name
),
suggestion: Some(
"use `source file as ...` for a `watch` glob, or remove the clause".to_owned(),
),
});
}
}
let is_http = source.provider.name == "http";
if is_http && source.url.is_none() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: source.span,
message: format!(
"`http` source `{}` requires a `url` clause",
source.name.name
),
suggestion: Some("add `url \"https://example.com/feed.json\"`".to_owned()),
});
}
if is_http {
if let Some(url) = &source.url {
let scheme_ok = url.value.starts_with("http://") || url.value.starts_with("https://");
if !scheme_ok {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: url.span,
message: format!(
"`http` source `{}` url `{}` is not an absolute http(s) URL",
source.name.name, url.value
),
suggestion: Some(
"use an absolute `http://` or `https://` URL the runtime can GET"
.to_owned(),
),
});
}
}
}
if !is_http {
if let Some(url) = &source.url {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: url.span,
message: format!(
"source `{}` declares a `url` clause but its provider is `{}`, not `http`",
source.name.name, source.provider.name
),
suggestion: Some(
"use `source http as ...` for a `url`, or remove the clause".to_owned(),
),
});
}
}
let is_clock = source.clock.is_some();
let is_file = source.provider.name == "file";
let is_http = source.provider.name == "http";
let mut dedup_field = None;
if let Some(dedup) = &source.dedup {
let span = match dedup {
SourceValue::Path { span, .. } => *span,
SourceValue::String(literal) => literal.span,
SourceValue::Number(_, span) => *span,
};
if !(is_http || is_file && source.watch.is_none()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"source `{}` declares a `dedup` clause but its provider is `{}`{}",
source.name.name,
source.provider.name,
if is_file {
" in `watch` mode, which is already content-keyed"
} else {
"; `dedup` applies to `file` (line mode) and `http` sources"
}
),
suggestion: Some("remove the `dedup` clause".to_owned()),
});
} else {
match dedup {
SourceValue::Path {
binding, segments, ..
} if binding.name == source.observe_binding.name && segments.len() == 1 => {
dedup_field = Some(segments[0].name.clone());
}
_ => {
diagnostics.push(Diagnostic {
related: Vec::new(),
span,
message: format!(
"source `{}` `dedup` must name one observation field off the \
`observe` binding (e.g. `dedup {}.line`)",
source.name.name, source.observe_binding.name
),
suggestion: Some(format!(
"the observation binding is `{}` (declared by `observe as {}`)",
source.observe_binding.name, source.observe_binding.name
)),
});
}
}
}
}
let path = source.path.as_ref().map(|literal| literal.value.clone());
let watch = source.watch.as_ref().map(|literal| literal.value.clone());
let url = source.url.as_ref().map(|literal| literal.value.clone());
let recurrence = source.clock.as_ref().map(|clock| clock.recurrence.clone());
let timezone = source
.clock
.as_ref()
.and_then(|clock| clock.timezone.as_ref().map(|tz| tz.value.clone()));
let missed = source.clock.as_ref().and_then(|clock| clock.missed);
ir.sources.push(IrSource {
name: source.name.name,
provider: source.provider.name,
is_clock,
is_file,
is_http,
recurrence,
timezone,
missed,
path,
watch,
url,
dedup_field,
observe_binding: source.observe_binding.name,
emit_signal: source.emit.signal,
emit_from: source.emit.from.as_ref().map(|ident| ident.name.clone()),
emit_fields: source
.emit
.fields
.into_iter()
.map(|field| IrSourceEmitField {
name: field.name.name,
value: field.value,
span: field.span,
})
.collect(),
span: source.span,
});
}
fn lower_event(event: EventDecl, ir: &mut IrProgram, diagnostics: &mut Vec<Diagnostic>) {
if ir.events.iter().any(|existing| existing.name == event.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: event.name_span,
message: format!("signal `{}` is declared more than once", event.name),
suggestion: Some("remove the duplicate signal declaration".to_owned()),
});
}
let mut fields = BTreeSet::new();
for field in &event.fields {
if !fields.insert(field.name.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: field.name.span,
message: format!(
"signal `{}` declares field `{}` more than once",
event.name, field.name.name
),
suggestion: Some(
"remove the duplicate field or give it a distinct name".to_owned(),
),
});
}
}
validate_presence_conditions(&event.name, &event.fields, diagnostics);
ir.events.push(IrEvent {
name: event.name,
fields: event
.fields
.into_iter()
.map(|field| IrClassField {
name: field.name.name,
ty: lower_type(field.ty),
is_key: false,
presence_condition: field.presence_condition,
span: field.span,
})
.collect(),
span: event.span,
});
}
fn lower_class(
class_decl: ClassDecl,
ir: &mut IrProgram,
schema_names: &BTreeSet<String>,
agent_names: &BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut fields = BTreeSet::new();
for field in &class_decl.fields {
if !fields.insert(field.name.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: field.name.span,
message: format!(
"class `{}` declares field `{}` more than once",
class_decl.name.name, field.name.name
),
suggestion: Some(
"remove the duplicate field or give it a distinct name".to_owned(),
),
});
}
validate_type_refs(&field.ty, schema_names, agent_names, diagnostics);
}
let key_fields = class_decl
.fields
.iter()
.filter(|field| field.is_key)
.collect::<Vec<_>>();
if key_fields.len() > 1 {
for field in key_fields.into_iter().skip(1) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: field.span,
message: format!(
"class `{}` declares more than one `@key` field",
class_decl.name.name
),
suggestion: Some("a class has at most one `@key` natural key in v0".to_owned()),
});
}
}
validate_presence_conditions(&class_decl.name.name, &class_decl.fields, diagnostics);
ir.schemas.push(IrSchema::Class(IrClass {
name: class_decl.name.name,
span: class_decl.span,
fields: class_decl
.fields
.into_iter()
.map(|field| IrClassField {
name: field.name.name,
ty: lower_type(field.ty),
is_key: field.is_key,
presence_condition: field.presence_condition,
span: field.span,
})
.collect(),
}));
}
fn lower_table(
table: TableDecl,
semantic: &SemanticContext,
workflow_contract_names: &WorkflowContractNames,
ir: &mut IrProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
if !semantic.schemas.class_exists(&table.schema.name) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: table.schema.span,
message: format!(
"table `{}` targets unknown class `{}`",
table.name.name, table.schema.name
),
suggestion: Some("declare the class before seeding rows for it".to_owned()),
});
return;
}
if table.rows.is_empty() {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: table.span,
message: format!("table `{}` has no rows", table.name.name),
suggestion: Some("add at least one `{ ... }` row".to_owned()),
});
return;
}
let mut body = String::new();
for row in &table.rows {
push_line(&mut body, format!("record {} {{", table.schema.name));
push_block_body(&row.body.text, &mut body);
push_line(&mut body, "}");
body.push('\n');
}
if body.ends_with('\n') {
body.pop();
}
let rule = RuleDecl {
name: Ident {
name: format!("table_{}", table.name.name),
span: table.name.span,
},
tags: Vec::new(),
description: None,
whens: vec![WhenClause {
text: "started".to_owned(),
span: table.name.span,
}],
body: BlockSource {
text: body,
span: table.span,
},
span: table.span,
};
let record_sources = table
.rows
.iter()
.map(|row| IrRecordSource {
schema: table.schema.name.clone(),
construct: "table_row".to_owned(),
span: row.span,
})
.collect::<Vec<_>>();
let rule_name = rule.name.name.clone();
lower_rule(rule, semantic, workflow_contract_names, ir, diagnostics);
if let Some(rule) = ir
.rules
.iter_mut()
.rev()
.find(|rule| rule.name == rule_name)
{
rule.metadata.record_sources = record_sources;
}
}
fn lower_coerce(
coerce: CoerceDecl,
ir: &mut IrProgram,
schema_names: &BTreeSet<String>,
agent_names: &BTreeSet<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut params = BTreeSet::new();
for param in &coerce.params {
if !params.insert(param.name.name.clone()) {
diagnostics.push(Diagnostic {
related: Vec::new(),
span: param.name.span,
message: format!(
"coerce `{}` declares parameter `{}` more than once",
coerce.name.name, param.name.name
),
suggestion: Some(
"remove the duplicate parameter or give it a distinct name".to_owned(),
),
});
}
validate_type_refs(¶m.ty, schema_names, agent_names, diagnostics);
}
validate_type_refs(&coerce.output, schema_names, agent_names, diagnostics);
validate_coerce_prompt_content_type_annotations(&coerce, diagnostics);
validate_coerce_body_fields(&coerce, diagnostics);
ir.coerces.push(IrCoerce {
span: coerce.name.span,
name: coerce.name.name,
params: coerce
.params
.into_iter()
.map(|param| IrParam {
name: param.name.name,
ty: lower_type(param.ty),
})
.collect(),
output: lower_type(coerce.output),
provider: coerce_declared_provider(&coerce.body.text),
body: coerce.body.text,
});
}
fn lower_rule(
rule: RuleDecl,
semantic: &SemanticContext,
workflow_contract_names: &WorkflowContractNames,
ir: &mut IrProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
validate_canonical_rule_body_syntax(&rule, diagnostics);
let metadata = analyze_rule(&rule, semantic, diagnostics);
validate_workflow_terminal_actions(
&rule,
semantic,
&binding_types_for_rule(&rule),
&known_roots_for_rule(&rule),
workflow_contract_names,
diagnostics,
);
validate_effectful_self_trigger(&rule, &metadata, diagnostics);
validate_send_channels(&rule, semantic, diagnostics);
validate_message_from_channels(&rule, semantic, diagnostics);
validate_evidence_fact_not_matched(&rule, diagnostics);
validate_turn_access_grants(&rule, &metadata, diagnostics);
ir.rules.push(IrRule {
name: rule.name.name,
whens: rule.whens.into_iter().map(lower_when_clause).collect(),
body: rule.body.text,
metadata,
});
}
fn lower_when_clause(when: WhenClause) -> IrWhen {
let source = when.text;
let (pattern, guard_source) = split_when_guard(&source);
let pattern = pattern.to_owned();
let guard = guard_source.and_then(|guard_source| {
let guard_offset = source.find(guard_source).unwrap_or(0);
lower_expression(
guard_source,
SourceSpan {
start: when.span.start + guard_offset,
end: when.span.start + guard_offset + guard_source.len(),
},
)
});
IrWhen {
source,
pattern,
guard,
span: when.span,
}
}
pub(crate) fn lower_case_pattern(
pattern: &str,
scrutinee_type: &TypeSyntax,
semantic: &SemanticContext,
) -> Option<IrCasePattern> {
if is_fallback_pattern(pattern) {
return Some(IrCasePattern::Wildcard);
}
if pattern == "None" {
return Some(IrCasePattern::OptionalNone);
}
if let Some(binding) = pattern.strip_prefix("Some ").map(str::trim) {
if !binding.is_empty() {
return Some(IrCasePattern::OptionalSome {
binding: binding.to_owned(),
});
}
}
match scrutinee_type {
TypeSyntax::Ref { name } if semantic.schemas.enums.contains_key(&name.name) => {
let (variant, _) = sum_case_pattern_parts(pattern);
Some(IrCasePattern::EnumVariant(variant.to_owned()))
}
TypeSyntax::Union { .. } => parse_literal_expr(pattern).and_then(|literal| match literal {
LiteralExpr::String(value) => Some(IrCasePattern::LiteralString(value.to_owned())),
LiteralExpr::Ident(value) => Some(IrCasePattern::LiteralString(value.to_owned())),
_ => None,
}),
TypeSyntax::AgentRef { .. } => {
parse_literal_expr(pattern).and_then(|literal| match literal {
LiteralExpr::String(value) | LiteralExpr::Ident(value) => {
Some(IrCasePattern::Agent(value.to_owned()))
}
_ => None,
})
}
TypeSyntax::Optional { inner, .. } => lower_case_pattern(pattern, inner, semantic),
_ => None,
}
}
pub(crate) fn lower_type(ty: TypeSyntax) -> IrType {
match ty {
TypeSyntax::Primitive { name, .. } => IrType::Primitive(lower_primitive_type(&name)),
TypeSyntax::LiteralString { value, .. } => IrType::LiteralString(value),
TypeSyntax::Ref { name } => IrType::Ref(name.name),
TypeSyntax::AgentRef { agents, .. } => {
IrType::AgentRef(agents.into_iter().map(|agent| agent.name).collect())
}
TypeSyntax::Optional { inner, .. } => IrType::Optional(Box::new(lower_type(*inner))),
TypeSyntax::Array { inner, .. } => IrType::Array(Box::new(lower_type(*inner))),
TypeSyntax::Map { inner, .. } => IrType::Map(Box::new(lower_type(*inner))),
TypeSyntax::Union { variants, .. } => {
IrType::Union(variants.into_iter().map(lower_type).collect())
}
}
}
fn lower_primitive_type(name: &str) -> IrPrimitiveType {
match name {
"string" => IrPrimitiveType::String,
"int" => IrPrimitiveType::Int,
"float" => IrPrimitiveType::Float,
"bool" => IrPrimitiveType::Bool,
"null" => IrPrimitiveType::Null,
"duration" => IrPrimitiveType::Duration,
"time" => IrPrimitiveType::Time,
"image" => IrPrimitiveType::Image,
"audio" => IrPrimitiveType::Audio,
"pdf" => IrPrimitiveType::Pdf,
"video" => IrPrimitiveType::Video,
"secret" => IrPrimitiveType::Secret,
_ => IrPrimitiveType::String,
}
}