import type { CallArg, Expr } from "./ast.ts"; import { type GenerateCtx } from "./codegen.ts"; import { type SlotAllocator, type SubPipelineLowerer } from "./lookup-translation.ts"; export type StreamMethodResult = { /** Stages this method contributes, appended to the surrounding chain. */ stages: object[]; /** * True if the emitted stages replace the document and drop in-scope `let` * bindings. Threaded back to the caller so the outer pipeline ctx can * clear the let scope. Defaults to false. */ clearLets?: boolean; /** * If true, the caller drops the *immediately preceding* stage from the * accumulator before appending `stages`. Used by methods like * `.toReversed()` that rewrite the preceding `$sort` rather than appending * a new stage. Defaults to false. */ replacesPreviousStage?: boolean; }; export type StreamMethodDef = { /** JS method name (e.g. "slice"). */ name: string; /** * Validate the call's arg shape. Throw `CodegenError` (with `.pos`) for * any rejection branch. Called before `lower`; lowering may assume the * args have the shape the validator accepts. */ validate: (args: readonly CallArg[], callPos: number) => void; /** * Produce the stages this method contributes. * * `prevStages` is the read-only view of stages the chain has emitted so * far in this context (outer pipeline for `$$` chains; `$unionWith` * sub-pipeline body for `$$$.` chains). Methods that don't need to * peek (`.slice`, `.map`, …) simply ignore it. Methods that do * (`.toReversed`) can read the last stage and return * `replacesPreviousStage: true` so the caller drops it before appending. * * `allocSlot` allocates a fresh `__jsmql.__lookup` slot from the * surrounding pipeline's tracker — used by methods that need to * materialise embedded `$$$..find/filter(...)` lookups (e.g. * `.map`'s body). Each call to `allocSlot()` marks the pipeline as * having used the namespace so the trailing `$unset: "__jsmql"` cleanup * is emitted. `inSubPipeline` is true when the chain is being lowered * inside a `$unionWith.pipeline` body (i.e. the `$$$..` head); * methods that would otherwise produce nested `$lookup` stages use this * flag to surface the standard "nested lookup not yet supported" error. */ lower: (args: readonly CallArg[], ctx: GenerateCtx, callPos: number, lowerBlock: SubPipelineLowerer, prevStages: readonly object[], allocSlot: SlotAllocator, inSubPipeline: boolean) => StreamMethodResult; }; type ReduceAccumulator = { kind: "sum"; value: string | number; } | { kind: "max"; value: string; } | { kind: "min"; value: string; } | { kind: "first"; value: string; } | { kind: "last"; value: string; } | { kind: "push"; value: string; }; export type ReduceWrapEntry = { key: string; accumulator: ReduceAccumulator; pos: number; }; /** * Detect the wrap patterns that consume `$$.reduce(...)` back into the * stream. Two forms, both lowering to the same `$group` + `$replaceWith` * pair via `lowerReduceWrap`: * * 1. **Scalar wrap.** `$$ = [{ : $$.reduce(…, ), … }];` * The inner array element is an object literal; each entry is a direct * `$$.reduce(...)` call. One accumulator per entry. * * 2. **Object reducer.** `$$ = [$$.reduce((acc, d) => ({...acc, : , ...}), { : , ... })];` * The inner array element is the `$$.reduce(...)` call itself; the * reducer body returns an object literal whose keys become the * accumulator namespace. Each entry's value is pattern-matched the * same way as the scalar form, except `acc` is referenced as * `acc.` (not bare `acc`). * * The array-returning reducer form (`$$ = [$$.reduce(... => acc.concat(...), [])]`) * is **not** handled here — its lowering is `$match` + `$replaceWith` rather * than `$group`-shaped, so it has its own detector / lowering pair * (`detectArrayReducerWrap` / `lowerArrayReducerWrap`). * * Returns `null` for non-matching shapes (the caller falls through to the * other RHS handlers, including the array-reducer detector). Throws for * matching-but-malformed shapes so the user sees a precise error instead of * a generic "RHS must be …". */ export declare function detectReduceWrap(value: Expr): ReduceWrapEntry[] | null; export type DictBuildWrap = { /** Path on `d` for the dict-entry key (e.g. "id" or "user.email"). */ keyPath: string; /** Path on `d` for the dict-entry value, OR null when the value is the bare doc. */ valuePath: string | null; /** Lambda position for actionable errors. */ lambdaPos: number; }; /** * Detect the dict-build wrap form. Returns null if the shape doesn't match * (so `detectReduceWrap` / `detectArrayReducerWrap` can have a turn); * returns a `DictBuildWrap` when it does match cleanly. * * Deliberately narrow: requires exactly one computed-key entry (plus optional * leading `...acc` spread), key path rooted on the `d` param, value path or * bare `d`, and `{}` init. Anything richer (multiple computed keys, mixed * static + computed, computed key reading from `acc`) is not this pattern * and either lands in the existing object-reducer path or surfaces a clear * error there. */ export declare function detectDictBuildWrap(value: Expr): DictBuildWrap | null; /** * Lower a detected dict-build wrap to the `$group` + `$replaceWith` pair. * One internal-namespace slot (`__jsmqlDict`) collects the `{k, v}` pairs; * `$arrayToObject` folds the pair-array into the final dict. */ export declare function lowerDictBuildWrap(wrap: DictBuildWrap): object[]; /** * Emit the `$group` + `$replaceWith` pair for a detected `[{key: $$.reduce(…), …}]` * wrap. The `$group` collects every keyed accumulator under `_id: null`; the * trailing `$replaceWith` drops the `_id: null` field so the output stream is * a single doc with exactly the user-named keys. */ export declare function lowerReduceWrap(entries: readonly ReduceWrapEntry[]): object[]; export type ArrayReducerProject = { kind: "field"; path: string; } | { kind: "identity"; }; export type ArrayReducerWrap = { /** Identity (`acc.concat(d)`) or field-path projection (`acc.concat(d.)`). */ project: ArrayReducerProject; /** * When present, the lowering emits a `$match` stage before the projection * using this expression as the predicate body. Translated through * `lowerStreamFilterPredicate` in pipeline.ts (same engine `.filter` uses). */ condition: Expr | null; /** The reducer's per-doc parameter name (used to translate the condition). */ dParam: string; /** Lambda position (for actionable errors). */ lambdaPos: number; }; /** * Detect `$$ = $$.reduce(, [])` — the array-returning reducer form. * A reducer seeded with `[]` already returns an array, i.e. a stream, so it is * assigned **unbracketed**. Returns the classified `ArrayReducerWrap`, or null * for non-matching shapes (the caller falls through to other handlers). * * Throws for: the legacy **bracketed** form `$$ = [$$.reduce(…, [])]` (wrapping * a stream in `[ ]` yields `[[…]]` — nonsense; the throw points at the * unbracketed form); a non-empty seed array; and unrecognised reducer bodies. */ export declare function detectArrayReducerWrap(value: Expr): ArrayReducerWrap | null; /** Look up a registered stream method by name; null if not registered. */ export declare function lookupStreamMethod(name: string): StreamMethodDef | null; /** Names of all registered stream methods (for error messages). */ export declare function streamMethodNames(): readonly string[]; export type MethodCallNode = Extract; export type StreamChain = { /** The receiver at the innermost end of the chain (CollectionRef, DatabaseRef-rooted member access, etc.). */ root: Expr; /** Method calls in the order they apply (innermost first). */ methods: MethodCallNode[]; }; /** * Walk an Expr that's expected to be a chain of `.method(...)` calls and * separate the innermost receiver from the chain. Always succeeds — * non-MethodCall input returns `{ root: expr, methods: [] }`. Callers * inspect `root.type` to decide whether the chain is rooted at a * legitimate stream/collection receiver. */ export declare function collectStreamChain(expr: Expr): StreamChain; export {}; //# sourceMappingURL=stream-methods.d.ts.map