135 lines
3.7 KiB
TypeScript
135 lines
3.7 KiB
TypeScript
|
|
import type { EffectBackend } from "../../../../src/core/effects/backend";
|
||
|
|
import type { State } from "../../../../src/core/eval/machine";
|
||
|
|
import type { OpCall, Resumption } from "../../../../src/core/effects/opcall";
|
||
|
|
import type { Val } from "../../../../src/core/eval/values";
|
||
|
|
import type { BudgetTracker } from "../../../../src/package-bridge/governance";
|
||
|
|
|
||
|
|
type AmbAlternative = {
|
||
|
|
thunk: Val;
|
||
|
|
resumption: Resumption;
|
||
|
|
};
|
||
|
|
|
||
|
|
export type AmbStrategy = "DFS" | "FAIR";
|
||
|
|
|
||
|
|
export class AmbBackend implements EffectBackend {
|
||
|
|
readonly ops = ["amb.choose", "amb.fail"] as const;
|
||
|
|
|
||
|
|
private alternatives: AmbAlternative[] = [];
|
||
|
|
private strategy: AmbStrategy = "DFS";
|
||
|
|
|
||
|
|
constructor(private budget?: BudgetTracker) {}
|
||
|
|
|
||
|
|
setStrategy(strategy: AmbStrategy): void {
|
||
|
|
this.strategy = strategy;
|
||
|
|
}
|
||
|
|
|
||
|
|
reset(): void {
|
||
|
|
this.alternatives = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
hasAlternatives(): boolean {
|
||
|
|
return this.alternatives.length > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
setBudget(budget: BudgetTracker): void {
|
||
|
|
this.budget = budget;
|
||
|
|
}
|
||
|
|
|
||
|
|
async handle(state: State, opcall: OpCall): Promise<State> {
|
||
|
|
if (opcall.op === "amb.choose") {
|
||
|
|
return this.handleChoose(state, opcall);
|
||
|
|
}
|
||
|
|
if (opcall.op === "amb.fail") {
|
||
|
|
return this.handleFail(state, opcall);
|
||
|
|
}
|
||
|
|
throw new Error(`AmbBackend: unhandled op ${opcall.op}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleChoose(state: State, opcall: OpCall): State {
|
||
|
|
const thunks = opcall.args[0];
|
||
|
|
if (!thunks || thunks.tag !== "Vector") {
|
||
|
|
throw new Error("amb.choose: expected vector of thunks");
|
||
|
|
}
|
||
|
|
|
||
|
|
function consToArray(v: Val): Val[] {
|
||
|
|
const result: Val[] = [];
|
||
|
|
let cur = v;
|
||
|
|
while (cur.tag === "Vector" && cur.items.length === 2) {
|
||
|
|
result.push(cur.items[0]);
|
||
|
|
cur = cur.items[1];
|
||
|
|
}
|
||
|
|
if (cur.tag === "Vector" && cur.items.length > 2) {
|
||
|
|
return cur.items;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
const items = consToArray(thunks);
|
||
|
|
if (items.length === 0) {
|
||
|
|
return this.handleFail(state, {
|
||
|
|
op: "amb.fail",
|
||
|
|
args: [{ tag: "Str", s: "amb: no alternatives" } as Val],
|
||
|
|
resumption: opcall.resumption,
|
||
|
|
ctxDigest: opcall.ctxDigest,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let i = 1; i < items.length; i++) {
|
||
|
|
this.alternatives.push({
|
||
|
|
thunk: items[i],
|
||
|
|
resumption: opcall.resumption,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this.budget?.consumeAmbAttempt?.();
|
||
|
|
|
||
|
|
const firstThunk = items[0];
|
||
|
|
if (firstThunk.tag !== "Closure") {
|
||
|
|
throw new Error("amb.choose: thunk must be a closure");
|
||
|
|
}
|
||
|
|
|
||
|
|
const baseState: State | "Uncaught" | "OutOfBudget" =
|
||
|
|
opcall.resumption.invoke({ tag: "Unit" } as Val);
|
||
|
|
if (baseState === ("Uncaught" as any) || baseState === ("OutOfBudget" as any)) {
|
||
|
|
return baseState as any;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
...baseState,
|
||
|
|
control: { tag: "Expr", e: firstThunk.body },
|
||
|
|
env: firstThunk.env,
|
||
|
|
} as State;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleFail(_state: State, _opcall: OpCall): State {
|
||
|
|
if (this.alternatives.length === 0) {
|
||
|
|
throw new Error("amb: all alternatives exhausted");
|
||
|
|
}
|
||
|
|
|
||
|
|
this.budget?.consumeAmbAttempt?.();
|
||
|
|
|
||
|
|
const alt = this.alternatives.shift()!;
|
||
|
|
const thunk = alt.thunk;
|
||
|
|
const resumption = alt.resumption;
|
||
|
|
|
||
|
|
const baseState: State | "Uncaught" | "OutOfBudget" =
|
||
|
|
resumption.invoke({ tag: "Unit" } as Val);
|
||
|
|
if (baseState === ("Uncaught" as any) || baseState === ("OutOfBudget" as any)) {
|
||
|
|
return baseState as any;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (thunk.tag === "Closure") {
|
||
|
|
return {
|
||
|
|
...baseState,
|
||
|
|
control: { tag: "Expr", e: thunk.body },
|
||
|
|
env: thunk.env,
|
||
|
|
} as State;
|
||
|
|
}
|
||
|
|
if (thunk.tag === "Native") {
|
||
|
|
throw new Error("amb: Native thunks not yet supported");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error(`amb: invalid thunk type ${thunk.tag}`);
|
||
|
|
}
|
||
|
|
}
|