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.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # Stringent
2
+
3
+ A type-safe expression parser for TypeScript with compile-time validation and inference.
4
+
5
+ > **Warning**
6
+ > This library is under active development and not yet ready for production use. APIs may change.
7
+
8
+ ## Overview
9
+
10
+ Stringent parses and validates expressions like `values.password == values.confirmPassword` against a schema at both compile-time and runtime, with full TypeScript type inference for expression results.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install stringent
16
+ # or
17
+ pnpm add stringent
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Define Your Grammar
23
+
24
+ Use `defineNode` to create expression nodes with patterns, precedence, and result types:
25
+
26
+ ```typescript
27
+ import { defineNode, number, constVal, lhs, rhs, createParser } from 'stringent';
28
+
29
+ // Atomic: number literals
30
+ const numberLit = defineNode({
31
+ name: "number",
32
+ pattern: [number()],
33
+ precedence: "atom",
34
+ resultType: "number",
35
+ });
36
+
37
+ // Binary operators with precedence
38
+ const add = defineNode({
39
+ name: "add",
40
+ pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
41
+ precedence: 1, // Lower = binds looser
42
+ resultType: "number",
43
+ });
44
+
45
+ const mul = defineNode({
46
+ name: "mul",
47
+ pattern: [lhs("number").as("left"), constVal("*"), rhs("number").as("right")],
48
+ precedence: 2, // Higher = binds tighter
49
+ resultType: "number",
50
+ });
51
+ ```
52
+
53
+ ### Create a Parser
54
+
55
+ ```typescript
56
+ const parser = createParser([numberLit, add, mul] as const);
57
+
58
+ // Type-safe parsing - result type is inferred at compile-time
59
+ const result = parser.parse("1+2*3", {});
60
+ // ^? const result: [{ node: "add"; left: { node: "number"; value: "1" }; right: { node: "mul"; left: { node: "number"; value: "2" }; right: { node: "number"; value: "3" } } }, ""]
61
+ ```
62
+
63
+ ### Pattern Elements
64
+
65
+ | Pattern | Description |
66
+ |---------|-------------|
67
+ | `number()` | Matches numeric literals |
68
+ | `string(quotes)` | Matches quoted strings |
69
+ | `ident()` | Matches identifiers, resolves type from context |
70
+ | `constVal(value)` | Matches exact string (operators, keywords) |
71
+ | `lhs(constraint)` | Left operand (higher precedence, avoids left-recursion) |
72
+ | `rhs(constraint)` | Right operand (same precedence, right-associative) |
73
+ | `expr(constraint)` | Full expression (all grammar levels) |
74
+
75
+ Use `.as(name)` to capture pattern elements as named bindings in the AST:
76
+
77
+ ```typescript
78
+ lhs("number").as("left") // Captures left operand as "left" in the AST node
79
+ ```
80
+
81
+ ### Runtime Evaluation (Coming Soon)
82
+
83
+ > **Note**
84
+ > Runtime evaluation is not yet implemented.
85
+
86
+ Add `eval` to compute values at runtime:
87
+
88
+ ```typescript
89
+ const add = defineNode({
90
+ name: "add",
91
+ pattern: [lhs("number").as("left"), constVal("+"), rhs("number").as("right")],
92
+ precedence: 1,
93
+ resultType: "number",
94
+ eval: ({ left, right }) => left + right,
95
+ });
96
+ ```
97
+
98
+ ## Key Features
99
+
100
+ - **Compile-time validation**: Invalid expressions fail TypeScript compilation
101
+ - **Type inference**: Expression result types are inferred automatically
102
+ - **Operator precedence**: Correct parsing of complex expressions
103
+ - **Schema-aware**: Validates field references against your schema
104
+ - **Dual API**: Same parsing logic at compile-time (types) and runtime
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Parser Combinators
3
+ *
4
+ * Composable parsers that work at both runtime and compile-time.
5
+ * Parse methods are generic - return types computed from input literals.
6
+ * All combinators thread context ($) through to child parsers.
7
+ *
8
+ * IMPORTANT: All parse methods MUST be generic in context:
9
+ * parse<TInput extends string, $ extends Context>(input: TInput, $: $)
10
+ */
11
+ import type { Context } from "../context.js";
12
+ import type { IParser, ParseNumber, ParseString, ParseIdent, ParseConst, _Number, _String, _Ident, _Const } from "../primitive/index.js";
13
+ /** Static type for parsing any parser */
14
+ export type Parse<P, TInput extends string, $ extends Context> = P extends _Number ? ParseNumber<TInput> : P extends _String<infer Q> ? ParseString<Q, TInput> : P extends _Const<infer V> ? ParseConst<V, TInput> : P extends _Ident ? ParseIdent<TInput, $> : P extends _Union<infer Parsers> ? ParseUnion<Parsers, TInput, $> : P extends _Tuple<infer Parsers> ? ParseTuple<Parsers, TInput, $> : P extends _Optional<infer Parser> ? ParseOptional<Parser, TInput, $> : P extends _Many<infer Parser> ? ParseMany<Parser, TInput, $> : never;
15
+ /** Static type for Union parsing */
16
+ export type ParseUnion<TParsers extends IParser[], TInput extends string, $ extends Context> = TParsers extends [
17
+ infer First extends IParser,
18
+ ...infer Rest extends IParser[]
19
+ ] ? Parse<First, TInput, $> extends [infer R, infer Remaining extends string] ? [R, Remaining] : ParseUnion<Rest, TInput, $> : [];
20
+ /** Static type for Tuple parsing */
21
+ export type ParseTuple<TParsers extends IParser[], TInput extends string, $ extends Context, TAcc extends unknown[] = []> = TParsers extends [
22
+ infer First extends IParser,
23
+ ...infer Rest extends IParser[]
24
+ ] ? Parse<First, TInput, $> extends [infer R, infer Remaining extends string] ? ParseTuple<Rest, Remaining, $, [...TAcc, R]> : [] : [TAcc, TInput];
25
+ /** Static type for Optional parsing */
26
+ export type ParseOptional<TParser extends IParser, TInput extends string, $ extends Context> = Parse<TParser, TInput, $> extends [infer R, infer Remaining extends string] ? [R, Remaining] : [undefined, TInput];
27
+ /** Static type for Many parsing */
28
+ export type ParseMany<TParser extends IParser, TInput extends string, $ extends Context, TAcc extends unknown[] = []> = Parse<TParser, TInput, $> extends [infer R, infer Remaining extends string] ? Remaining extends TInput ? [TAcc, TInput] : ParseMany<TParser, Remaining, $, [...TAcc, R]> : [TAcc, TInput];
29
+ declare class _Union<TParsers extends IParser[]> {
30
+ readonly __combinator: "union";
31
+ readonly parsers: TParsers;
32
+ constructor(parsers: TParsers);
33
+ parse<TInput extends string, $ extends Context>(input: TInput, $: $): ParseUnion<TParsers, TInput, $>;
34
+ }
35
+ declare class _Tuple<TParsers extends IParser[]> {
36
+ readonly __combinator: "tuple";
37
+ readonly parsers: TParsers;
38
+ constructor(parsers: TParsers);
39
+ parse<TInput extends string, $ extends Context>(input: TInput, $: $): ParseTuple<TParsers, TInput, $>;
40
+ }
41
+ declare class _Optional<TParser extends IParser> {
42
+ readonly __combinator: "optional";
43
+ readonly parser: TParser;
44
+ constructor(parser: TParser);
45
+ parse<TInput extends string, $ extends Context>(input: TInput, $: $): ParseOptional<TParser, TInput, $>;
46
+ }
47
+ declare class _Many<TParser extends IParser> {
48
+ readonly __combinator: "many";
49
+ readonly parser: TParser;
50
+ constructor(parser: TParser);
51
+ parse<TInput extends string, $ extends Context>(input: TInput, $: $): ParseMany<TParser, TInput, $>;
52
+ }
53
+ export declare const Union: <TParsers extends IParser[]>(parsers: [...TParsers]) => _Union<TParsers>;
54
+ export declare const Tuple: <TParsers extends IParser[]>(parsers: [...TParsers]) => _Tuple<TParsers>;
55
+ export declare const Optional: <TParser extends IParser>(parser: TParser) => _Optional<TParser>;
56
+ export declare const Many: <TParser extends IParser>(parser: TParser) => _Many<TParser>;
57
+ export type { _Union, _Tuple, _Optional, _Many };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Parser Combinators
3
+ *
4
+ * Composable parsers that work at both runtime and compile-time.
5
+ * Parse methods are generic - return types computed from input literals.
6
+ * All combinators thread context ($) through to child parsers.
7
+ *
8
+ * IMPORTANT: All parse methods MUST be generic in context:
9
+ * parse<TInput extends string, $ extends Context>(input: TInput, $: $)
10
+ */
11
+ // =============================================================================
12
+ // Union Combinator
13
+ // =============================================================================
14
+ class _Union {
15
+ __combinator = "union";
16
+ parsers;
17
+ constructor(parsers) {
18
+ this.parsers = parsers;
19
+ }
20
+ parse(input, $) {
21
+ for (const parser of this.parsers) {
22
+ const result = parser.parse(input, $);
23
+ if (result.length === 2) {
24
+ return result;
25
+ }
26
+ }
27
+ return [];
28
+ }
29
+ }
30
+ // =============================================================================
31
+ // Tuple Combinator
32
+ // =============================================================================
33
+ class _Tuple {
34
+ __combinator = "tuple";
35
+ parsers;
36
+ constructor(parsers) {
37
+ this.parsers = parsers;
38
+ }
39
+ parse(input, $) {
40
+ const results = [];
41
+ let remaining = input;
42
+ for (const parser of this.parsers) {
43
+ const result = parser.parse(remaining, $);
44
+ if (result.length !== 2) {
45
+ return [];
46
+ }
47
+ results.push(result[0]);
48
+ remaining = result[1];
49
+ }
50
+ return [results, remaining];
51
+ }
52
+ }
53
+ // =============================================================================
54
+ // Optional Combinator
55
+ // =============================================================================
56
+ class _Optional {
57
+ __combinator = "optional";
58
+ parser;
59
+ constructor(parser) {
60
+ this.parser = parser;
61
+ }
62
+ parse(input, $) {
63
+ const result = this.parser.parse(input, $);
64
+ if (result.length === 2) {
65
+ return result;
66
+ }
67
+ return [undefined, input];
68
+ }
69
+ }
70
+ // =============================================================================
71
+ // Many Combinator
72
+ // =============================================================================
73
+ class _Many {
74
+ __combinator = "many";
75
+ parser;
76
+ constructor(parser) {
77
+ this.parser = parser;
78
+ }
79
+ parse(input, $) {
80
+ const results = [];
81
+ let remaining = input;
82
+ while (true) {
83
+ const result = this.parser.parse(remaining, $);
84
+ if (result.length !== 2) {
85
+ break;
86
+ }
87
+ results.push(result[0]);
88
+ const newRemaining = result[1];
89
+ // Prevent infinite loop on zero-width matches
90
+ if (newRemaining === remaining) {
91
+ break;
92
+ }
93
+ remaining = newRemaining;
94
+ }
95
+ return [results, remaining];
96
+ }
97
+ }
98
+ // =============================================================================
99
+ // Exported Factories
100
+ // =============================================================================
101
+ export const Union = (parsers) => new _Union(parsers);
102
+ export const Tuple = (parsers) => new _Tuple(parsers);
103
+ export const Optional = (parser) => new _Optional(parser);
104
+ export const Many = (parser) => new _Many(parser);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Context - carries schema data for identifier type resolution
3
+ *
4
+ * The context maps variable names to their types, enabling type-safe
5
+ * parsing of expressions like `x + y` where x and y come from a schema.
6
+ *
7
+ * Grammar is now computed from node schemas via ComputeGrammar<Nodes>.
8
+ */
9
+ /**
10
+ * Parse context with schema data.
11
+ *
12
+ * @typeParam TData - Schema mapping variable names to their types
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * type Ctx = Context<{ x: "number"; y: "string" }>;
17
+ * // x resolves to type "number", y resolves to type "string"
18
+ * ```
19
+ */
20
+ export interface Context<TData extends Record<string, string> = Record<string, string>> {
21
+ /** Schema types for identifier resolution */
22
+ readonly data: TData;
23
+ }
24
+ /** Empty context (no schema variables) */
25
+ export declare const emptyContext: Context<{}>;
26
+ /** Type alias for empty context */
27
+ export type EmptyContext = Context<{}>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Context - carries schema data for identifier type resolution
3
+ *
4
+ * The context maps variable names to their types, enabling type-safe
5
+ * parsing of expressions like `x + y` where x and y come from a schema.
6
+ *
7
+ * Grammar is now computed from node schemas via ComputeGrammar<Nodes>.
8
+ */
9
+ // =============================================================================
10
+ // Default Context
11
+ // =============================================================================
12
+ /** Empty context (no schema variables) */
13
+ export const emptyContext = { data: {} };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * createParser Entry Point
3
+ *
4
+ * Creates a type-safe parser from node schemas.
5
+ * The returned parser has:
6
+ * - Type-level parsing via Parse<Grammar, Input, Context>
7
+ * - Runtime parsing that mirrors the type structure
8
+ */
9
+ import type { NodeSchema } from "./schema/index.js";
10
+ import type { ComputeGrammar, Grammar } from "./grammar/index.js";
11
+ import type { Parse } from "./parse/index.js";
12
+ import type { Context } from "./context.js";
13
+ /**
14
+ * Parser interface with type-safe parse method.
15
+ *
16
+ * TGrammar: The computed grammar type from node schemas
17
+ * TNodes: The tuple of node schemas
18
+ */
19
+ export interface Parser<TGrammar extends Grammar, TNodes extends readonly NodeSchema[]> {
20
+ /**
21
+ * Parse an input string.
22
+ *
23
+ * @param input - The input string to parse
24
+ * @param schema - Schema mapping field names to their types
25
+ * @returns Parse result with computed type
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const result = parser.parse("1+2", {});
30
+ * // Type: Parse<Grammar, "1+2", Context<{}>>
31
+ * // Value: [{ type: "binary", name: "add", left: {...}, right: {...} }, ""]
32
+ * ```
33
+ */
34
+ parse<TInput extends string, TSchema extends Record<string, string>>(input: ValidatedInput<TGrammar, TInput, Context<TSchema>>, schema: TSchema): Parse<TGrammar, TInput, Context<TSchema>>;
35
+ /** The node schemas used to create this parser */
36
+ readonly nodes: TNodes;
37
+ }
38
+ type ValidatedInput<TGrammar extends Grammar, TInput extends string, $ extends Context> = Parse<TGrammar, TInput, $> extends [any, any] ? TInput : never;
39
+ /**
40
+ * Create a type-safe parser from node schemas.
41
+ *
42
+ * The returned parser has both:
43
+ * - Compile-time type inference via Parse<Grammar, Input, Context>
44
+ * - Runtime parsing that matches the type structure
45
+ *
46
+ * @param nodes - Tuple of node schemas defining the grammar
47
+ * @returns Parser instance with type-safe parse method
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { defineNode, number, expr, constVal, createParser } from "stringent";
52
+ import { Validate } from '../dist/static/parser';
53
+ *
54
+ * const numberLit = defineNode({
55
+ * name: "number",
56
+ * pattern: [number()],
57
+ * precedence: "atom",
58
+ * resultType: "number",
59
+ * });
60
+ *
61
+ * const add = defineNode({
62
+ * name: "add",
63
+ * pattern: [expr("number"), constVal("+"), expr("number")],
64
+ * precedence: 1,
65
+ * resultType: "number",
66
+ * });
67
+ *
68
+ * const parser = createParser([numberLit, add] as const);
69
+ *
70
+ * // Type-safe parsing!
71
+ * const result = parser.parse("1+2", {});
72
+ * // Type: [BinaryNode<"add", NumberNode<"1">, NumberNode<"2">, "number">, ""]
73
+ * ```
74
+ */
75
+ export declare function createParser<const TNodes extends readonly NodeSchema[]>(nodes: TNodes): Parser<ComputeGrammar<TNodes>, TNodes>;
76
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * createParser Entry Point
3
+ *
4
+ * Creates a type-safe parser from node schemas.
5
+ * The returned parser has:
6
+ * - Type-level parsing via Parse<Grammar, Input, Context>
7
+ * - Runtime parsing that mirrors the type structure
8
+ */
9
+ import { parse as runtimeParse } from "./runtime/parser.js";
10
+ // =============================================================================
11
+ // createParser Factory
12
+ // =============================================================================
13
+ /**
14
+ * Create a type-safe parser from node schemas.
15
+ *
16
+ * The returned parser has both:
17
+ * - Compile-time type inference via Parse<Grammar, Input, Context>
18
+ * - Runtime parsing that matches the type structure
19
+ *
20
+ * @param nodes - Tuple of node schemas defining the grammar
21
+ * @returns Parser instance with type-safe parse method
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { defineNode, number, expr, constVal, createParser } from "stringent";
26
+ import { Validate } from '../dist/static/parser';
27
+ *
28
+ * const numberLit = defineNode({
29
+ * name: "number",
30
+ * pattern: [number()],
31
+ * precedence: "atom",
32
+ * resultType: "number",
33
+ * });
34
+ *
35
+ * const add = defineNode({
36
+ * name: "add",
37
+ * pattern: [expr("number"), constVal("+"), expr("number")],
38
+ * precedence: 1,
39
+ * resultType: "number",
40
+ * });
41
+ *
42
+ * const parser = createParser([numberLit, add] as const);
43
+ *
44
+ * // Type-safe parsing!
45
+ * const result = parser.parse("1+2", {});
46
+ * // Type: [BinaryNode<"add", NumberNode<"1">, NumberNode<"2">, "number">, ""]
47
+ * ```
48
+ */
49
+ export function createParser(nodes) {
50
+ return {
51
+ parse(input, schema) {
52
+ const context = { data: schema };
53
+ return runtimeParse(nodes, input, context);
54
+ },
55
+ nodes,
56
+ };
57
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Grammar Type Computation
3
+ *
4
+ * Computes a grammar TYPE from node schemas. The grammar is a flat tuple
5
+ * of precedence levels, sorted from lowest to highest precedence, with
6
+ * atoms as the final element.
7
+ *
8
+ * Example:
9
+ * [[AddOps], [MulOps], [Atoms]]
10
+ * // level 0 (lowest prec) → level 1 → atoms (last)
11
+ */
12
+ import type { Pipe, Numbers, Objects, Tuples, Fn, Unions, Call } from "hotscript";
13
+ import type { NodeSchema } from "../schema/index.js";
14
+ /**
15
+ * A grammar is a tuple of levels, where each level is an array of node schemas.
16
+ * Sorted by precedence (lowest first), atoms last.
17
+ */
18
+ export type Grammar = readonly (readonly NodeSchema[])[];
19
+ /**
20
+ * Compare precedence entries: numbers sort ascending, "atom" always comes last.
21
+ * Entry format: [precedence, nodes[]]
22
+ */
23
+ interface SortByPrecedence extends Fn {
24
+ return: this["arg0"][0] extends "atom" ? false : this["arg1"][0] extends "atom" ? true : Call<Numbers.LessThanOrEqual, this["arg0"][0], this["arg1"][0]>;
25
+ }
26
+ /**
27
+ * Compute the grammar tuple from node schemas.
28
+ *
29
+ * 1. Group nodes by precedence
30
+ * 2. Convert to entries and sort (numbers ascending, "atom" last)
31
+ * 3. Extract just the node arrays
32
+ */
33
+ type ComputeGrammarImpl<TNodes extends readonly NodeSchema[]> = Pipe<[
34
+ ...TNodes
35
+ ], [
36
+ Tuples.GroupBy<Objects.Get<"precedence">>,
37
+ Objects.Entries,
38
+ Unions.ToTuple,
39
+ Tuples.Sort<SortByPrecedence>,
40
+ Tuples.Map<Tuples.At<1>>
41
+ ]>;
42
+ export type ComputeGrammar<TNodes extends readonly NodeSchema[]> = ComputeGrammarImpl<TNodes> extends infer G extends Grammar ? G : never;
43
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Grammar Type Computation
3
+ *
4
+ * Computes a grammar TYPE from node schemas. The grammar is a flat tuple
5
+ * of precedence levels, sorted from lowest to highest precedence, with
6
+ * atoms as the final element.
7
+ *
8
+ * Example:
9
+ * [[AddOps], [MulOps], [Atoms]]
10
+ * // level 0 (lowest prec) → level 1 → atoms (last)
11
+ */
12
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stringent - Type-safe Expression Parser
3
+ *
4
+ * Main entry points:
5
+ * - defineNode: Create grammar node schemas
6
+ * - createParser: Build a type-safe parser from nodes
7
+ * - Parse<Grammar, Input, Context>: Type-level parsing
8
+ */
9
+ export { defineNode, number, string, ident, constVal, lhs, rhs, expr } from "./schema/index.js";
10
+ export type { NodeSchema, PatternSchema, NumberSchema, StringSchema, IdentSchema, ConstSchema, ExprSchema, ExprRole, Precedence, ConfigureFn, EvalFn, SchemaToType, InferBindings, InferEvaluatedBindings, } from "./schema/index.js";
11
+ export { createParser } from "./createParser.js";
12
+ export type { Parser } from "./createParser.js";
13
+ export type { Parse, BinaryNode, ParseError, TypeMismatchError, NoMatchError } from "./parse/index.js";
14
+ export type { ComputeGrammar, Grammar } from "./grammar/index.js";
15
+ export type { Context, EmptyContext } from "./context.js";
16
+ export { emptyContext } from "./context.js";
17
+ export type { ASTNode, LiteralNode, NumberNode, StringNode, IdentNode, ConstNode, } from "./primitive/index.js";
18
+ export { Number, String, Ident, Const, type IParser, type ParseResult, } from "./primitive/index.js";
19
+ export { Union, Tuple, Optional, Many } from "./combinators/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stringent - Type-safe Expression Parser
3
+ *
4
+ * Main entry points:
5
+ * - defineNode: Create grammar node schemas
6
+ * - createParser: Build a type-safe parser from nodes
7
+ * - Parse<Grammar, Input, Context>: Type-level parsing
8
+ */
9
+ // =============================================================================
10
+ // Main API: defineNode & createParser
11
+ // =============================================================================
12
+ export { defineNode, number, string, ident, constVal, lhs, rhs, expr } from "./schema/index.js";
13
+ export { createParser } from "./createParser.js";
14
+ export { emptyContext } from "./context.js";
15
+ // =============================================================================
16
+ // Legacy Primitives (for backwards compatibility)
17
+ // =============================================================================
18
+ export { Number, String, Ident, Const, } from "./primitive/index.js";
19
+ // =============================================================================
20
+ // Legacy Combinators (for backwards compatibility)
21
+ // =============================================================================
22
+ export { Union, Tuple, Optional, Many } from "./combinators/index.js";