stringent 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Runtime Parser
3
+ *
4
+ * Mirrors the type-level Parse<Grammar, Input, Context> at runtime.
5
+ * Uses the same precedence-based parsing strategy:
6
+ * 1. Try operators at current level (lowest precedence first)
7
+ * 2. Fall back to next level (higher precedence)
8
+ * 3. Base case: try atoms (last level)
9
+ */
10
+ import { Token } from "@sinclair/parsebox";
11
+ // =============================================================================
12
+ // Primitive Parsers
13
+ // =============================================================================
14
+ function parseNumber(input) {
15
+ const result = Token.Number(input);
16
+ if (result.length === 0)
17
+ return [];
18
+ return [
19
+ {
20
+ node: "literal",
21
+ raw: result[0],
22
+ value: +result[0],
23
+ outputSchema: "number",
24
+ },
25
+ result[1],
26
+ ];
27
+ }
28
+ function parseString(quotes, input) {
29
+ const result = Token.String([...quotes], input);
30
+ if (result.length === 0)
31
+ return [];
32
+ return [
33
+ {
34
+ node: "literal",
35
+ raw: result[0],
36
+ value: result[0],
37
+ outputSchema: "string",
38
+ },
39
+ result[1],
40
+ ];
41
+ }
42
+ function parseIdent(input, context) {
43
+ const result = Token.Ident(input);
44
+ if (result.length === 0)
45
+ return [];
46
+ const name = result[0];
47
+ const valueType = name in context.data
48
+ ? context.data[name]
49
+ : "unknown";
50
+ return [
51
+ { node: "identifier", name, outputSchema: valueType },
52
+ result[1],
53
+ ];
54
+ }
55
+ function parseConst(value, input) {
56
+ const result = Token.Const(value, input);
57
+ if (result.length === 0)
58
+ return [];
59
+ return [{ node: "const", outputSchema: JSON.stringify(value) }, result[1]];
60
+ }
61
+ // =============================================================================
62
+ // Build Runtime Grammar from Node Schemas
63
+ // =============================================================================
64
+ /**
65
+ * Build runtime grammar from node schemas.
66
+ *
67
+ * Returns a flat tuple of levels:
68
+ * [[ops@prec1], [ops@prec2], ..., [atoms]]
69
+ *
70
+ * Levels are sorted by precedence ascending (lowest first).
71
+ * Atoms are always the last level.
72
+ */
73
+ export function buildGrammar(nodes) {
74
+ const atoms = [];
75
+ const operators = new Map();
76
+ for (const node of nodes) {
77
+ if (node.precedence === "atom") {
78
+ atoms.push(node);
79
+ }
80
+ else {
81
+ const prec = node.precedence;
82
+ if (!operators.has(prec)) {
83
+ operators.set(prec, []);
84
+ }
85
+ operators.get(prec).push(node);
86
+ }
87
+ }
88
+ // Sort precedences ascending
89
+ const precedences = [...operators.keys()].sort((a, b) => a - b);
90
+ // Build flat grammar: [[ops@prec1], [ops@prec2], ..., [atoms]]
91
+ const grammar = [];
92
+ for (const prec of precedences) {
93
+ grammar.push(operators.get(prec));
94
+ }
95
+ grammar.push(atoms);
96
+ return grammar;
97
+ }
98
+ // =============================================================================
99
+ // Pattern Element Parsing
100
+ // =============================================================================
101
+ /**
102
+ * Parse a single pattern element (non-Expr).
103
+ */
104
+ function parseElement(element, input, context) {
105
+ switch (element.kind) {
106
+ case "number":
107
+ return parseNumber(input);
108
+ case "string":
109
+ return parseString(element.quotes, input);
110
+ case "ident":
111
+ return parseIdent(input, context);
112
+ case "const":
113
+ return parseConst(element.value, input);
114
+ default:
115
+ return [];
116
+ }
117
+ }
118
+ /**
119
+ * Parse an expression element based on its role.
120
+ *
121
+ * Role determines which grammar slice is used:
122
+ * - "lhs": nextLevels (avoids left-recursion)
123
+ * - "rhs": currentLevels (maintains precedence, enables right-associativity)
124
+ * - "expr": fullGrammar (full reset for delimited contexts)
125
+ */
126
+ function parseElementWithLevel(element, input, context, currentLevels, nextLevels, fullGrammar) {
127
+ if (element.kind === "expr") {
128
+ const exprElement = element;
129
+ const constraint = exprElement.constraint;
130
+ const role = exprElement.role;
131
+ if (role === "lhs") {
132
+ return parseExprWithConstraint(nextLevels, input, context, constraint, fullGrammar);
133
+ }
134
+ else if (role === "rhs") {
135
+ return parseExprWithConstraint(currentLevels, input, context, constraint, fullGrammar);
136
+ }
137
+ else {
138
+ return parseExprWithConstraint(fullGrammar, input, context, constraint, fullGrammar);
139
+ }
140
+ }
141
+ return parseElement(element, input, context);
142
+ }
143
+ /**
144
+ * Parse a pattern tuple.
145
+ */
146
+ function parsePatternTuple(pattern, input, context, currentLevels, nextLevels, fullGrammar) {
147
+ let remaining = input;
148
+ const children = [];
149
+ for (const element of pattern) {
150
+ const result = parseElementWithLevel(element, remaining, context, currentLevels, nextLevels, fullGrammar);
151
+ if (result.length === 0)
152
+ return [];
153
+ children.push(result[0]);
154
+ remaining = result[1];
155
+ }
156
+ return [children, remaining];
157
+ }
158
+ /**
159
+ * Extract named bindings from pattern and children.
160
+ * Only includes children where the pattern element has .as(name).
161
+ */
162
+ function extractBindings(pattern, children) {
163
+ const bindings = {};
164
+ for (let i = 0; i < pattern.length; i++) {
165
+ const element = pattern[i];
166
+ const child = children[i];
167
+ // Check if element is a NamedSchema (has __named and name properties)
168
+ if ("__named" in element && element.__named === true) {
169
+ bindings[element.name] = child;
170
+ }
171
+ }
172
+ return bindings;
173
+ }
174
+ /**
175
+ * Build AST node from parsed children.
176
+ *
177
+ * Uses named bindings from .as() to determine node fields.
178
+ * - Single child without names: passthrough (atom behavior)
179
+ * - If configure() provided: transform bindings to fields
180
+ * - Otherwise: bindings become node fields directly
181
+ */
182
+ function buildNodeResult(nodeSchema, children, context) {
183
+ const bindings = extractBindings(nodeSchema.pattern, children);
184
+ // Single unnamed child → passthrough (atom behavior)
185
+ if (Object.keys(bindings).length === 0 && children.length === 1) {
186
+ return children[0];
187
+ }
188
+ // Apply configure() if provided, otherwise use bindings directly
189
+ const fields = nodeSchema.configure
190
+ ? nodeSchema.configure(bindings, context)
191
+ : bindings;
192
+ // Build node with fields
193
+ return {
194
+ node: nodeSchema.name,
195
+ outputSchema: nodeSchema.resultType,
196
+ ...fields,
197
+ };
198
+ }
199
+ /**
200
+ * Parse a node pattern.
201
+ */
202
+ function parseNodePattern(node, input, context, currentLevels, nextLevels, fullGrammar) {
203
+ const result = parsePatternTuple(node.pattern, input, context, currentLevels, nextLevels, fullGrammar);
204
+ if (result.length === 0)
205
+ return [];
206
+ return [buildNodeResult(node, result[0], context), result[1]];
207
+ }
208
+ /**
209
+ * Parse with expression constraint check.
210
+ */
211
+ function parseExprWithConstraint(startLevels, input, context, constraint, fullGrammar) {
212
+ const result = parseLevels(startLevels, input, context, fullGrammar);
213
+ if (result.length === 0)
214
+ return [];
215
+ const [node, remaining] = result;
216
+ if (constraint !== undefined) {
217
+ const nodeOutputSchema = node.outputSchema;
218
+ if (nodeOutputSchema !== constraint) {
219
+ return [];
220
+ }
221
+ }
222
+ return [node, remaining];
223
+ }
224
+ /**
225
+ * Try parsing each node in a level.
226
+ */
227
+ function parseNodes(nodes, input, context, currentLevels, nextLevels, fullGrammar) {
228
+ for (const node of nodes) {
229
+ const result = parseNodePattern(node, input, context, currentLevels, nextLevels, fullGrammar);
230
+ if (result.length === 2)
231
+ return result;
232
+ }
233
+ return [];
234
+ }
235
+ /**
236
+ * Parse using grammar levels (flat tuple).
237
+ *
238
+ * levels[0] is current level, levels[1:] is next levels.
239
+ * Base case: single level (atoms) - just try those nodes.
240
+ */
241
+ function parseLevels(levels, input, context, fullGrammar) {
242
+ if (levels.length === 0) {
243
+ return [];
244
+ }
245
+ const currentNodes = levels[0];
246
+ const nextLevels = levels.slice(1);
247
+ // Try nodes at current level
248
+ const result = parseNodes(currentNodes, input, context, levels, nextLevels, fullGrammar);
249
+ if (result.length === 2) {
250
+ return result;
251
+ }
252
+ // Fall through to next levels (if any)
253
+ if (nextLevels.length > 0) {
254
+ return parseLevels(nextLevels, input, context, fullGrammar);
255
+ }
256
+ return [];
257
+ }
258
+ // =============================================================================
259
+ // Public API
260
+ // =============================================================================
261
+ /**
262
+ * Parse input string using node schemas.
263
+ *
264
+ * The return type is computed from the input types using the type-level
265
+ * Parse<Grammar, Input, Context> type, ensuring runtime and type-level
266
+ * parsing stay in sync.
267
+ */
268
+ export function parse(nodes, input, context) {
269
+ const grammar = buildGrammar(nodes);
270
+ return parseLevels(grammar, input, context, grammar);
271
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Schema Types
3
+ *
4
+ * Pattern element schemas for defineNode. These are pure type descriptors
5
+ * that preserve literal types for compile-time grammar computation.
6
+ *
7
+ * The key insight: defineNode returns a schema object whose TYPE carries
8
+ * all the information needed for type-level parsing. No runtime magic.
9
+ */
10
+ import type { NumberNode, StringNode, IdentNode, ConstNode } from "../primitive/index.js";
11
+ export interface Schema<TKind extends string> {
12
+ readonly kind: TKind;
13
+ }
14
+ /** Number literal pattern element */
15
+ export interface NumberSchema extends Schema<"number"> {
16
+ }
17
+ /** String literal pattern element */
18
+ export interface StringSchema<TQuotes extends readonly string[] = readonly string[]> extends Schema<"string"> {
19
+ readonly quotes: TQuotes;
20
+ }
21
+ /** Identifier pattern element */
22
+ export interface IdentSchema extends Schema<"ident"> {
23
+ }
24
+ /** Constant (exact match) pattern element */
25
+ export interface ConstSchema<TValue extends string = string> extends Schema<"const"> {
26
+ readonly value: TValue;
27
+ }
28
+ /** Expression role determines which grammar level is used */
29
+ export type ExprRole = "lhs" | "rhs" | "expr";
30
+ /** Recursive expression pattern element with optional type constraint and role */
31
+ export interface ExprSchema<
32
+ /** This technically shouldn't require a string
33
+ * because JSON or array or other types could be used in the future.
34
+ * For now, I would prefer that we just keep it open tbh. And validate at the
35
+ * expr()/lhs()/rhs() usage site by
36
+ * using arktype.type.validate<> (see createBox example: https://arktype.io/docs/generics).
37
+ **/
38
+ TConstraint extends string = string, TRole extends ExprRole = ExprRole> extends Schema<"expr"> {
39
+ readonly constraint?: TConstraint;
40
+ readonly role: TRole;
41
+ }
42
+ /** Base pattern schema type (without NamedSchema to avoid circular reference) */
43
+ export type PatternSchemaBase = NumberSchema | StringSchema<readonly string[]> | IdentSchema | ConstSchema<string> | ExprSchema<string, ExprRole>;
44
+ /**
45
+ * A pattern schema with a binding name.
46
+ * Created by calling .as(name) on any pattern element.
47
+ *
48
+ * Uses intersection so schema properties remain accessible without unwrapping.
49
+ *
50
+ * @example
51
+ * lhs("number").as("left") // ExprSchema<"number", "lhs"> & { __named: true; name: "left" }
52
+ */
53
+ export type NamedSchema<TSchema extends PatternSchemaBase = PatternSchemaBase, TName extends string = string> = TSchema & {
54
+ readonly __named: true;
55
+ readonly name: TName;
56
+ };
57
+ /** Union of all pattern element schemas (including named) */
58
+ export type PatternSchema = PatternSchemaBase | NamedSchema;
59
+ /**
60
+ * Schema wrapper with .as() method for naming bindings.
61
+ * All pattern factories return this type.
62
+ */
63
+ export type SchemaWithAs<TSchema extends PatternSchemaBase> = TSchema & {
64
+ /** Add a binding name to this pattern element */
65
+ as<TName extends string>(name: TName): NamedSchema<TSchema, TName>;
66
+ };
67
+ /** Create a number literal pattern element */
68
+ export declare const number: () => NumberSchema & {
69
+ as<TName extends string>(name: TName): NamedSchema<NumberSchema, TName>;
70
+ };
71
+ /** Create a string literal pattern element */
72
+ export declare const string: <const TQuotes extends readonly string[]>(quotes: TQuotes) => StringSchema<TQuotes> & {
73
+ as<TName extends string>(name: TName): NamedSchema<StringSchema<TQuotes>, TName>;
74
+ };
75
+ /** Create an identifier pattern element */
76
+ export declare const ident: () => IdentSchema & {
77
+ as<TName extends string>(name: TName): NamedSchema<IdentSchema, TName>;
78
+ };
79
+ /** Create a constant (exact match) pattern element */
80
+ export declare const constVal: <const TValue extends string>(value: TValue) => ConstSchema<TValue> & {
81
+ as<TName extends string>(name: TName): NamedSchema<ConstSchema<TValue>, TName>;
82
+ };
83
+ /**
84
+ * Create a LEFT-HAND SIDE expression element.
85
+ *
86
+ * Uses TNextLevel grammar to avoid left-recursion.
87
+ * Must be at position 0 in a pattern.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const add = defineNode({
92
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
93
+ * // lhs parses at higher precedence to avoid infinite recursion
94
+ * });
95
+ * ```
96
+ */
97
+ export declare const lhs: <const TConstraint extends string>(constraint?: TConstraint) => ExprSchema<TConstraint, "lhs"> & {
98
+ as<TName extends string>(name: TName): NamedSchema<ExprSchema<TConstraint, "lhs">, TName>;
99
+ };
100
+ /**
101
+ * Create a RIGHT-HAND SIDE expression element.
102
+ *
103
+ * Uses TCurrentLevel grammar to maintain precedence and enable right-associativity.
104
+ * Used for right operands of binary operators.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const add = defineNode({
109
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
110
+ * // rhs parses at same level for right-associativity: 1+2+3 = 1+(2+3)
111
+ * });
112
+ * ```
113
+ */
114
+ export declare const rhs: <const TConstraint extends string>(constraint?: TConstraint) => ExprSchema<TConstraint, "rhs"> & {
115
+ as<TName extends string>(name: TName): NamedSchema<ExprSchema<TConstraint, "rhs">, TName>;
116
+ };
117
+ /**
118
+ * Create a FULL expression element.
119
+ *
120
+ * Uses TFullGrammar - resets to full grammar (precedence 0).
121
+ * Used for delimited contexts like parentheses, ternary branches, function arguments.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const parens = defineNode({
126
+ * pattern: [constVal("("), expr().as("inner"), constVal(")")],
127
+ * // expr() can contain ANY expression, including low-precedence operators
128
+ * });
129
+ *
130
+ * const ternary = defineNode({
131
+ * pattern: [lhs("boolean").as("cond"), constVal("?"), expr().as("then"), constVal(":"), expr().as("else")],
132
+ * // The branches can contain any expression
133
+ * });
134
+ * ```
135
+ */
136
+ export declare const expr: <const TConstraint extends string>(constraint?: TConstraint) => ExprSchema<TConstraint, "expr"> & {
137
+ as<TName extends string>(name: TName): NamedSchema<ExprSchema<TConstraint, "expr">, TName>;
138
+ };
139
+ /** Precedence type: number for operators, "atom" for atoms (literals) */
140
+ export type Precedence = number | "atom";
141
+ /**
142
+ * A node definition schema.
143
+ *
144
+ * This is the return type of defineNode. The generic parameters capture
145
+ * all the literal types needed for compile-time grammar computation:
146
+ * - TName: The unique node name (e.g., "add", "mul")
147
+ * - TPattern: The pattern elements as a tuple type
148
+ * - TPrecedence: The precedence (number for operators, "atom" for atoms)
149
+ * - TResultType: The result type (e.g., "number", "string")
150
+ */
151
+ export interface NodeSchema<TName extends string = string, TPattern extends readonly PatternSchema[] = readonly PatternSchema[], TPrecedence extends Precedence = Precedence, TResultType extends string = string> {
152
+ readonly name: TName;
153
+ readonly pattern: TPattern;
154
+ readonly precedence: TPrecedence;
155
+ readonly resultType: TResultType;
156
+ /**
157
+ * Optional: Transform parsed bindings into node fields.
158
+ * If not provided, bindings are used directly as fields.
159
+ *
160
+ * @param bindings - The named values extracted from pattern via .as()
161
+ * @param ctx - The parse context
162
+ * @returns The fields to add to the AST node
163
+ */
164
+ readonly configure?: ConfigureFn;
165
+ /**
166
+ * Optional: Evaluate the AST node to produce a runtime value.
167
+ *
168
+ * @param node - The full AST node including name, outputSchema, and fields
169
+ * @param ctx - The evaluation context
170
+ * @returns The evaluated value
171
+ */
172
+ readonly eval?: EvalFn;
173
+ }
174
+ /** Stored function type for configure - loose for variance compatibility */
175
+ export type ConfigureFn = <$>(bindings: Record<string, unknown>, ctx: $) => Record<string, unknown>;
176
+ /** Stored function type for eval - loose for variance compatibility */
177
+ export type EvalFn = <$>(values: Record<string, unknown>, ctx: $) => unknown;
178
+ /**
179
+ * Define a node type for the grammar.
180
+ *
181
+ * The `const` modifier on generics ensures literal types are preserved:
182
+ * - name: "add" (not string)
183
+ * - pattern: readonly [ExprSchema<"number">, ConstSchema<"+">, ExprSchema<"number">]
184
+ * - precedence: 1 or "atom" (not number | "atom")
185
+ * - resultType: "number" (not string)
186
+ *
187
+ * @example
188
+ * const add = defineNode({
189
+ * name: "add",
190
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
191
+ * precedence: 1,
192
+ * resultType: "number",
193
+ * eval: ({ left, right }) => left + right, // left and right are already numbers
194
+ * });
195
+ */
196
+ export declare function defineNode<const TName extends string, const TPattern extends readonly PatternSchema[], const TPrecedence extends Precedence, const TResultType extends string>(config: {
197
+ readonly name: TName;
198
+ readonly pattern: TPattern;
199
+ readonly precedence: TPrecedence;
200
+ readonly resultType: TResultType;
201
+ readonly configure?: <$>(bindings: InferBindings<TPattern>, ctx: $) => Record<string, unknown>;
202
+ readonly eval?: <$>(values: InferEvaluatedBindings<TPattern>, ctx: $) => SchemaToType<TResultType>;
203
+ }): NodeSchema<TName, TPattern, TPrecedence, TResultType>;
204
+ /** Extract the operator from a binary pattern (second element should be ConstSchema) */
205
+ export type ExtractOperator<T extends readonly PatternSchema[]> = T extends readonly [
206
+ PatternSchema,
207
+ infer Op extends ConstSchema,
208
+ PatternSchema
209
+ ] ? Op["value"] : never;
210
+ /**
211
+ * Map a schema type string to its TypeScript runtime type.
212
+ * Used for eval return types and evaluated bindings.
213
+ */
214
+ export type SchemaToType<T extends string> = T extends "number" ? number : T extends "string" ? string : T extends "boolean" ? boolean : unknown;
215
+ /**
216
+ * Infer the AST node type from a pattern schema.
217
+ * This maps schema types to their corresponding node types.
218
+ *
219
+ * Note: ExprSchema maps to `unknown` since the actual type depends on the constraint
220
+ * and what's parsed. The runtime parser will fill this in.
221
+ */
222
+ export type InferNodeType<TSchema extends PatternSchemaBase> = TSchema extends NumberSchema ? NumberNode : TSchema extends StringSchema ? StringNode : TSchema extends IdentSchema ? IdentNode : TSchema extends ConstSchema ? ConstNode : TSchema extends ExprSchema<infer C> ? {
223
+ outputSchema: C;
224
+ } : never;
225
+ /**
226
+ * Infer the evaluated value type from a pattern schema.
227
+ * For ExprSchema, uses the constraint to determine the runtime type.
228
+ */
229
+ export type InferEvaluatedType<TSchema extends PatternSchemaBase> = TSchema extends NumberSchema ? number : TSchema extends StringSchema ? string : TSchema extends IdentSchema ? unknown : TSchema extends ConstSchema ? never : TSchema extends ExprSchema<infer C extends string> ? SchemaToType<C> : never;
230
+ /**
231
+ * Extract all NamedSchema entries from a pattern tuple as a union.
232
+ */
233
+ type ExtractNamedSchemas<TPattern extends readonly PatternSchema[]> = TPattern[number] extends infer E ? E extends NamedSchema<infer S, infer N> ? {
234
+ schema: S;
235
+ name: N;
236
+ } : never : never;
237
+ /**
238
+ * Infer bindings object type from a pattern (AST nodes).
239
+ * Used for configure() - receives parsed AST nodes.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * type Pattern = [
244
+ * NamedSchema<ExprSchema<"number", "lhs">, "left">,
245
+ * ConstSchema<"+">,
246
+ * NamedSchema<ExprSchema<"number", "rhs">, "right">
247
+ * ];
248
+ * type Bindings = InferBindings<Pattern>;
249
+ * // { left: { outputSchema: "number" }; right: { outputSchema: "number" } }
250
+ * ```
251
+ */
252
+ export type InferBindings<TPattern extends readonly PatternSchema[]> = {
253
+ [K in ExtractNamedSchemas<TPattern> as K["name"]]: InferNodeType<K["schema"]>;
254
+ };
255
+ /**
256
+ * Infer evaluated bindings from a pattern (runtime values).
257
+ * Used for eval() - receives already-evaluated values.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * type Pattern = [
262
+ * NamedSchema<ExprSchema<"number", "lhs">, "left">,
263
+ * ConstSchema<"+">,
264
+ * NamedSchema<ExprSchema<"number", "rhs">, "right">
265
+ * ];
266
+ * type EvalBindings = InferEvaluatedBindings<Pattern>;
267
+ * // { left: number; right: number }
268
+ * ```
269
+ */
270
+ export type InferEvaluatedBindings<TPattern extends readonly PatternSchema[]> = {
271
+ [K in ExtractNamedSchemas<TPattern> as K["name"]]: InferEvaluatedType<K["schema"]>;
272
+ };
273
+ export {};
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Schema Types
3
+ *
4
+ * Pattern element schemas for defineNode. These are pure type descriptors
5
+ * that preserve literal types for compile-time grammar computation.
6
+ *
7
+ * The key insight: defineNode returns a schema object whose TYPE carries
8
+ * all the information needed for type-level parsing. No runtime magic.
9
+ */
10
+ /** Create a schema wrapper with .as() method */
11
+ function withAs(schema) {
12
+ return Object.assign(schema, {
13
+ as(name) {
14
+ return { ...schema, __named: true, name };
15
+ },
16
+ });
17
+ }
18
+ // =============================================================================
19
+ // Pattern Element Factories
20
+ // =============================================================================
21
+ /** Create a number literal pattern element */
22
+ export const number = () => withAs({ kind: "number" });
23
+ /** Create a string literal pattern element */
24
+ export const string = (quotes) => withAs({ kind: "string", quotes });
25
+ /** Create an identifier pattern element */
26
+ export const ident = () => withAs({ kind: "ident" });
27
+ /** Create a constant (exact match) pattern element */
28
+ export const constVal = (value) => withAs({ kind: "const", value });
29
+ /**
30
+ * Create a LEFT-HAND SIDE expression element.
31
+ *
32
+ * Uses TNextLevel grammar to avoid left-recursion.
33
+ * Must be at position 0 in a pattern.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const add = defineNode({
38
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
39
+ * // lhs parses at higher precedence to avoid infinite recursion
40
+ * });
41
+ * ```
42
+ */
43
+ export const lhs = (constraint) => withAs({
44
+ kind: "expr",
45
+ constraint: constraint,
46
+ role: "lhs",
47
+ });
48
+ /**
49
+ * Create a RIGHT-HAND SIDE expression element.
50
+ *
51
+ * Uses TCurrentLevel grammar to maintain precedence and enable right-associativity.
52
+ * Used for right operands of binary operators.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const add = defineNode({
57
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
58
+ * // rhs parses at same level for right-associativity: 1+2+3 = 1+(2+3)
59
+ * });
60
+ * ```
61
+ */
62
+ export const rhs = (constraint) => withAs({
63
+ kind: "expr",
64
+ constraint: constraint,
65
+ role: "rhs",
66
+ });
67
+ /**
68
+ * Create a FULL expression element.
69
+ *
70
+ * Uses TFullGrammar - resets to full grammar (precedence 0).
71
+ * Used for delimited contexts like parentheses, ternary branches, function arguments.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const parens = defineNode({
76
+ * pattern: [constVal("("), expr().as("inner"), constVal(")")],
77
+ * // expr() can contain ANY expression, including low-precedence operators
78
+ * });
79
+ *
80
+ * const ternary = defineNode({
81
+ * pattern: [lhs("boolean").as("cond"), constVal("?"), expr().as("then"), constVal(":"), expr().as("else")],
82
+ * // The branches can contain any expression
83
+ * });
84
+ * ```
85
+ */
86
+ export const expr = (constraint) => withAs({
87
+ kind: "expr",
88
+ constraint: constraint,
89
+ role: "expr",
90
+ });
91
+ /**
92
+ * Define a node type for the grammar.
93
+ *
94
+ * The `const` modifier on generics ensures literal types are preserved:
95
+ * - name: "add" (not string)
96
+ * - pattern: readonly [ExprSchema<"number">, ConstSchema<"+">, ExprSchema<"number">]
97
+ * - precedence: 1 or "atom" (not number | "atom")
98
+ * - resultType: "number" (not string)
99
+ *
100
+ * @example
101
+ * const add = defineNode({
102
+ * name: "add",
103
+ * pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
104
+ * precedence: 1,
105
+ * resultType: "number",
106
+ * eval: ({ left, right }) => left + right, // left and right are already numbers
107
+ * });
108
+ */
109
+ export function defineNode(config) {
110
+ return config;
111
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Type-Level Inference
3
+ *
4
+ * Infer<AST, Context> computes the result type of a parsed AST.
5
+ *
6
+ * With the new architecture, nodes carry their outputSchema directly,
7
+ * so inference is simpler - just extract the outputSchema field.
8
+ */
9
+ import type { Context } from "../context.js";
10
+ /**
11
+ * Infer the result type of an AST node.
12
+ *
13
+ * Nodes now carry their outputSchema directly:
14
+ * - NumberNode has outputSchema: "number"
15
+ * - BinaryNode<Name, Left, Right, OutputSchema> has outputSchema: OutputSchema
16
+ * - IdentNode<Name, OutputSchema> has outputSchema: OutputSchema
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * type Num = Infer<NumberNode<"42">, {}>; // "number"
21
+ * type Add = Infer<BinaryNode<"add", NumberNode<"1">, NumberNode<"2">, "number">, {}>; // "number"
22
+ * ```
23
+ */
24
+ export type Infer<AST, $ extends Context> = AST extends {
25
+ outputSchema: infer R extends string;
26
+ } ? R : never;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Type-Level Inference
3
+ *
4
+ * Infer<AST, Context> computes the result type of a parsed AST.
5
+ *
6
+ * With the new architecture, nodes carry their outputSchema directly,
7
+ * so inference is simpler - just extract the outputSchema field.
8
+ */
9
+ export {};