@hatua/expressions 0.0.0 → 0.1.0

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 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { CallNode, Expression, HoleNode, IndexNode, LiteralNode, MemberNode, NameNode, ProjectNode, TemplateNode, TextNode, UnaryNode } from './ast.js';
2
+ /** What the parser hands an action: a match, a list of matches, or nothing. */
3
+ type Matched = unknown;
4
+ /** Flatten whatever a match produced into a string. Stands in for Peggy's `$`. */
5
+ export declare function str(...parts: Matched[]): string;
6
+ export declare function templateNode(segs: Matched): TemplateNode;
7
+ export declare function holeNode(at: Matched, expr: Matched): HoleNode;
8
+ export declare function textNode(chars: Matched): TextNode;
9
+ export declare function ternaryNode(cond: Matched, tail: Matched): Expression;
10
+ /**
11
+ * Fold `head ( op operand )*` left. PEG has no left recursion, so precedence is
12
+ * an explicit rule cascade and associativity is decided here — which is
13
+ * precisely why the parse scenarios are separate from the eval ones.
14
+ */
15
+ export declare function binaryNode(head: Matched, tail: Matched): Expression;
16
+ export declare function unaryNode(at: Matched, op: Matched, operand: Matched): UnaryNode;
17
+ /** Parentheses group; they leave no node behind. */
18
+ export declare function parenNode(expr: Matched): Expression;
19
+ /**
20
+ * Attach each suffix to what precedes it.
21
+ *
22
+ * A suffix is parsed knowing only where its own punctuation is — the `.`, the
23
+ * `[`, the `(` — but a node's `at` is documented as the offset of its *first*
24
+ * character, and `a.b` starts at `a`. So the offset is restamped here, once the
25
+ * object is known, which is the only point at which it can be.
26
+ *
27
+ * It matters because `at` is what an editor squiggles and what a runner logs. A
28
+ * diagnostic about `s9.name` that points at the dot sends the reader to the
29
+ * middle of the thing that failed.
30
+ */
31
+ export declare function postfixNode(base: Matched, suffixes: Matched): Expression;
32
+ export declare function memberSuffix(at: Matched, name: Matched): Omit<MemberNode, 'object'>;
33
+ export declare function projectSuffix(at: Matched): Omit<ProjectNode, 'object'>;
34
+ export declare function indexSuffix(at: Matched, index: Matched): Omit<IndexNode, 'object'>;
35
+ export declare function callSuffix(at: Matched, args: Matched): Omit<CallNode, 'object'>;
36
+ export declare function argList(head: Matched, tail: Matched): Matched[];
37
+ export declare function nameNode(at: Matched, name: Matched): NameNode;
38
+ export declare function nullNode(at: Matched): LiteralNode;
39
+ export declare function boolNode(at: Matched, value: Matched): LiteralNode;
40
+ export declare function numberNode(at: Matched, int: Matched, frac: Matched, exp: Matched): LiteralNode;
41
+ export declare function stringNode(at: Matched, chars: Matched): LiteralNode;
42
+ export declare function escapeChar(char: Matched): string;
43
+ export {};
@@ -0,0 +1,23 @@
1
+ import { Expression, TemplateNode } from './ast.js';
2
+ import { Diagnostic } from './errors.js';
3
+ /** Parse a whole Template. Throws `ExpressionError` carrying EXPR_PARSE_ERROR. */
4
+ export declare function parseTemplate(source: string): TemplateNode;
5
+ /**
6
+ * Parse one Expression, with no surrounding `{{ }}`.
7
+ *
8
+ * Only the conformance corpus and tooling need this; a field value is always a
9
+ * whole Template. It exists because precedence and associativity bugs live in
10
+ * the parse rather than the evaluation, and they are invisible to evaluation
11
+ * scenarios whenever two parsers build different trees that happen to evaluate
12
+ * alike on the sample data — the most dangerous divergence there is, because it
13
+ * passes everything until one workflow hits the disagreeing case.
14
+ */
15
+ export declare function parseExpression(source: string): Expression;
16
+ /** Parse, returning the diagnostic instead of throwing. */
17
+ export declare function tryParseTemplate(source: string): {
18
+ ok: true;
19
+ template: TemplateNode;
20
+ } | {
21
+ ok: false;
22
+ diagnostics: readonly Diagnostic[];
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { Expression, TemplateNode } from './ast.js';
2
+ /**
3
+ * Whether an expression is exactly a path.
4
+ *
5
+ * Indexing with a literal counts: `s2.messages[0].subject` still names one
6
+ * value and nothing more. A call does not — the moment something is computed,
7
+ * there is no target to retarget.
8
+ */
9
+ export declare function isReference(node: Expression): boolean;
10
+ /** The path a Reference names, or null when the expression is not one. */
11
+ export declare function referencePath(node: Expression): string | null;
12
+ /**
13
+ * The Reference a whole Template holds, when it holds exactly one and nothing
14
+ * else — which is the case the builder renders as a pill.
15
+ *
16
+ * `Hi {{ steps.s2.name }}` is not one: it is text with a hole in it, and the pill
17
+ * belongs inside the field rather than instead of it.
18
+ */
19
+ export declare function templateReference(template: TemplateNode): string | null;
20
+ /** The same, from source, for callers that have not parsed anything yet. */
21
+ export declare function sourceReference(template: string): string | null;
22
+ /**
23
+ * Every Reference inside an expression, outermost first.
24
+ *
25
+ * Outermost, because `s2.messages[].subject` is one Reference and not four:
26
+ * descending into a node that is already one would name its own prefix a second
27
+ * time.
28
+ *
29
+ * Here rather than beside either caller, because both of them ask the same
30
+ * question of the same grammar — the builder to draw each Reference as a pill,
31
+ * a rename to find the ones it invalidates — and two walks of an expression
32
+ * tree are two chances to forget a node kind. A node kind nobody descends into
33
+ * is a Reference nothing draws and a rename silently skips.
34
+ */
35
+ export declare function referencesIn(node: Expression): Expression[];
36
+ /**
37
+ * Rewrite every Reference under one rooted path, and return the Template.
38
+ *
39
+ * `renamePath(t, 'var.old', 'var.new')` repairs `{{ var.old }}` and
40
+ * `{{ text.upper(var.old) + 1 }}` alike: the walk is over Reference nodes, not
41
+ * over whether the Template *is* one, so a computed hole is rewritten exactly as
42
+ * a bare path is. A rewrite keyed on `templateReference` would repair only the
43
+ * simplest holes and silently skip every interesting one.
44
+ *
45
+ * **Prefixes end at a segment boundary.** Renaming `var.to` leaves `var.total`
46
+ * alone: a path matches when it is `from`, or continues with `.` or `[`.
47
+ *
48
+ * ## It declines rather than guesses
49
+ *
50
+ * ADR-0008 gives this grammar two generators and no AST→text, so nothing here
51
+ * reconstructs an expression from its tree. The source is copied through and
52
+ * only stretches checked character for character against the path the tree
53
+ * reports are swapped — the discipline `expressionChip` follows to draw a pill.
54
+ *
55
+ * Where the two disagree — `{{ var . old }}`, which parses and whose node offset
56
+ * holds something other than `var.old` — that occurrence is returned untouched.
57
+ * There is no way to know which stretch to replace without writing text the
58
+ * grammar cannot produce, and a rename that guessed would corrupt a file Hatua
59
+ * does not own (ADR-0001). A missed occurrence goes stale and is reported, which
60
+ * is the state every consumer already handles (ADR-0021).
61
+ *
62
+ * A Template that does not parse is returned unchanged: a command runs against
63
+ * documents that do not project, and half-written text is one of them.
64
+ */
65
+ export declare function renamePath(source: string, from: string, to: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ import { FunctionSpec } from '#generated/builtins.js';
2
+ import { Expression } from './ast.js';
3
+ import { Value, ValueType } from './value.js';
4
+ /**
5
+ * What a path does when it resolves to nothing.
6
+ *
7
+ * It governs path *resolution* only, never operator semantics — which is what
8
+ * keeps the truth tables single-valued. `null` still cannot be ordered, whether
9
+ * it came from a missing key or from a key holding null.
10
+ */
11
+ export type OnMissing = 'error' | 'null';
12
+ /** A named Template plus the type it must produce. */
13
+ export interface Slot {
14
+ readonly name: string;
15
+ readonly template: string;
16
+ readonly expectedType: ValueType;
17
+ }
18
+ /**
19
+ * The buckets a path can resolve into, one per root.
20
+ *
21
+ * Every root is a key here and nothing resolves outside one, which is what
22
+ * makes `root()` a table rather than a table with a fallback: a name the
23
+ * evaluator does not recognise is missing, not a step id to go looking for.
24
+ * ADR-0014 is the reason a step id is never at the root — `steps.run` and
25
+ * `run.id` are different buckets, so neither can shadow the other and no
26
+ * resolution order can decide which wins.
27
+ */
28
+ export interface EvaluationContext {
29
+ /** Step outputs, keyed by step id, addressed as `steps.<id>.…`. */
30
+ readonly steps?: Readonly<Record<string, Value>>;
31
+ /** Trigger payloads, addressed as `triggers.<id>.…`. */
32
+ readonly triggers?: Readonly<Record<string, Value>>;
33
+ /**
34
+ * The values a Block was called with, addressed as `params.<k>`.
35
+ *
36
+ * Supplied per invocation rather than per run: a Block called twice is called
37
+ * with different arguments, and its parameters are the only part of scope
38
+ * that changes between two calls of the same Block.
39
+ */
40
+ readonly params?: Readonly<Record<string, Value>>;
41
+ /** Workflow variables, addressed as `var.<key>`. */
42
+ readonly var?: Readonly<Record<string, Value>>;
43
+ /** The Host's ambient values for this execution, addressed as `run.<key>`. */
44
+ readonly run?: Readonly<Record<string, Value>>;
45
+ /** Which Trigger fired. Needed when several are declared. */
46
+ readonly TRIGGER?: string | null;
47
+ /**
48
+ * The clock `dt.now()` reads.
49
+ *
50
+ * Never the system clock: an expression that reads the wall clock is
51
+ * unfixturable, and two steps in one run would disagree about when "now" was.
52
+ */
53
+ readonly now?: Date;
54
+ readonly onMissing?: OnMissing;
55
+ readonly functions?: FunctionRegistry;
56
+ }
57
+ /** How a function is implemented. Arguments arrive evaluated and checked. */
58
+ export type FunctionImpl = (args: readonly Value[], context: EvaluationContext) => Value;
59
+ /**
60
+ * A function is its declaration *and* its implementation, together.
61
+ *
62
+ * Keeping them paired is what lets arity and argument types be enforced in one
63
+ * place rather than at the top of thirty-four implementations, and it is why a
64
+ * Host's functions need no special handling: a Host declaration produces the
65
+ * same pair.
66
+ */
67
+ export interface RegisteredFunction {
68
+ readonly spec: FunctionSpec;
69
+ readonly impl: FunctionImpl;
70
+ }
71
+ export type FunctionRegistry = ReadonlyMap<string, RegisteredFunction>;
72
+ /**
73
+ * Resolve one Slot.
74
+ *
75
+ * A Template that is exactly one hole keeps the expression's own type — the
76
+ * number 24, not the string "24". Anything else interpolates, because mixed
77
+ * text can only be text.
78
+ */
79
+ export declare function resolve(context: EvaluationContext, slot: Slot): Value;
80
+ /**
81
+ * Resolve a whole `with:` map in one call.
82
+ *
83
+ * It reports every failure together rather than stopping at the first, because
84
+ * a user fixing one field at a time is a user running the workflow five times
85
+ * to find five mistakes.
86
+ */
87
+ export declare function resolveAll(context: EvaluationContext, slots: readonly Slot[]): Record<string, Value>;
88
+ export declare function evaluate(node: Expression, context: EvaluationContext): Value;
89
+ /**
90
+ * `==` and `!=` are total: every pair of values has an answer, and no value is
91
+ * ever converted to reach it. Two different types are simply not equal.
92
+ */
93
+ export declare function equals(left: Value, right: Value): boolean;
94
+ /** Reconstruct the source path, for a diagnostic that has to name what failed. */
95
+ export declare function pathText(node: Expression): string;
package/dist/sexp.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Expression, TemplateNode } from './ast.js';
2
+ export interface SexpOptions {
3
+ /** Append `@offset` to every node. Only the offset scenarios ask for this. */
4
+ readonly offsets?: boolean;
5
+ }
6
+ export declare function templateToSexp(template: TemplateNode, options?: SexpOptions): string;
7
+ export declare function toSexp(node: Expression, options?: SexpOptions): string;
@@ -0,0 +1,39 @@
1
+ import { ValueType } from './value.js';
2
+ /**
3
+ * The declared shape of something addressable.
4
+ *
5
+ * `members` describes an object's members, or — for a `list` — the fields of
6
+ * each element, which is exactly what a Component Manifest's `of:` means.
7
+ */
8
+ export interface TypeNode {
9
+ readonly type: ValueType;
10
+ readonly members?: Readonly<Record<string, TypeNode>>;
11
+ }
12
+ /**
13
+ * One thing an expression may name, and what it yields.
14
+ *
15
+ * Scope arrives as an argument rather than being derived here, so this package
16
+ * depends on `@hatua/schema` and nothing else — `@hatua/model` builds these
17
+ * from the document and the manifests, and no cycle appears between them.
18
+ */
19
+ export interface ScopeEntry {
20
+ /** The token root: `s2`, `triggers.nightly`, `var.digest_to`, `TRIGGER`. */
21
+ readonly path: string;
22
+ readonly type: TypeNode;
23
+ }
24
+ /** The outcome of checking an expression's type against a field's. */
25
+ export type TypeVerdict = 'matches' | 'conflicts' | 'unknown';
26
+ /**
27
+ * Compare a statically-determined type against a declared one.
28
+ *
29
+ * The coercion permitted here is exactly the coercion `satisfies` permits at run
30
+ * time, stated once at the level of types rather than values: any scalar into
31
+ * `text`, `null` into anything, and otherwise an exact match.
32
+ */
33
+ export declare function match(actual: ValueType, declared: ValueType): TypeVerdict;
34
+ /** Types that can be ordered with `<`, `<=`, `>`, `>=`. */
35
+ export declare const ORDERED_TYPES: readonly ValueType[];
36
+ /** Whether a statically-known type could be an operand of an ordered comparison. */
37
+ export declare const canOrder: (type: ValueType) => boolean;
38
+ /** The element shape of a list, which the manifest spells as the list's own `of:`. */
39
+ export declare const elementOf: (node: TypeNode) => TypeNode;
@@ -0,0 +1,32 @@
1
+ import { FunctionSpec } from '#generated/builtins.js';
2
+ import { Expression } from './ast.js';
3
+ import { Diagnostic } from './errors.js';
4
+ import { ScopeEntry } from './types.js';
5
+ import { ValueType } from './value.js';
6
+ /**
7
+ * Everything design-time checking needs of a function: what it declares.
8
+ *
9
+ * Wider than `FunctionRegistry`, which pairs each declaration with the
10
+ * implementation that runs it. Checking never calls one — it reads the spec for
11
+ * arity and argument types and nothing else — so requiring the pair would force
12
+ * a checker to carry every implementation in the bundle to answer a question
13
+ * none of them is asked. A `FunctionRegistry` satisfies this, so a runner that
14
+ * already has one passes it unchanged.
15
+ */
16
+ export type FunctionDeclarations = ReadonlyMap<string, {
17
+ readonly spec: FunctionSpec;
18
+ }>;
19
+ export interface CheckContext {
20
+ /** What this step may address. Sibling branches are deliberately absent. */
21
+ readonly scope: readonly ScopeEntry[];
22
+ /** Hatua's functions merged with the Host's. */
23
+ readonly functions: FunctionDeclarations;
24
+ }
25
+ /**
26
+ * Check one Template against the type its field declares.
27
+ *
28
+ * Returns everything it found. Callers decide what to do with it: the Inspector
29
+ * renders all of them, Publish looks only at the errors.
30
+ */
31
+ export declare function validate(template: string, expectedType: ValueType, context: CheckContext): Diagnostic[];
32
+ export declare function inferType(node: Expression, context: CheckContext, found: Diagnostic[]): ValueType;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The value space.
3
+ *
4
+ * Two rules make everything else in this package tractable, and both are
5
+ * pinned by ADR-0009:
6
+ *
7
+ * - There is exactly one absent value, `null`. A missing key yields it, and
8
+ * reading a property of it yields it again. Nothing here ever produces
9
+ * `undefined`.
10
+ * - There is one numeric type and it is a 64-bit float, so `7 / 2` is 3.5 in
11
+ * both languages. Go must never reach for `int`.
12
+ *
13
+ * `NaN` and `Infinity` are not in the space at all: division by zero is an
14
+ * error rather than a value, which is what keeps them out.
15
+ */
16
+ /**
17
+ * Every type a value or a declaration can name.
18
+ *
19
+ * The first seven are the Component Manifest's own output types, so a field's
20
+ * declared type and an expression's type are drawn from one vocabulary rather
21
+ * than two that have to be mapped. `unknown` and `null` exist only here.
22
+ */
23
+ export type ValueType = 'text' | 'number' | 'boolean' | 'datetime' | 'list' | 'object' | 'item' | 'unknown' | 'null';
24
+ export type Value = string | number | boolean | Date | readonly Value[] | {
25
+ readonly [key: string]: Value;
26
+ } | null;
27
+ /** The runtime type of a value, in the same vocabulary a manifest declares. */
28
+ export declare function typeOf(value: Value): ValueType;
29
+ /**
30
+ * Whether a value satisfies a declared type.
31
+ *
32
+ * Coercion at the boundary is narrow and declared, so "must match" has a
33
+ * precise meaning:
34
+ *
35
+ * - any scalar into `text` is permitted — a `text` field is the universal
36
+ * sink, and that is exactly what interpolation already does;
37
+ * - `text` into `number` is *not* implicit; it requires `num.parse()`;
38
+ * - `null` satisfies any declared type. Whether absence is *acceptable* is
39
+ * `req:`'s business, not the evaluator's;
40
+ * - everything else must match exactly.
41
+ */
42
+ export declare function satisfies(value: Value, declared: ValueType): boolean;
43
+ export declare function isScalar(type: ValueType): boolean;
44
+ /**
45
+ * Render a value as text at a `text` boundary.
46
+ *
47
+ * Numbers go through the ECMAScript `Number::toString` algorithm, which Go does
48
+ * not implement — `String(1e-6)` is `"0.000001"` in JavaScript and `"1e-06"` in
49
+ * Go. TypeScript gets it for free; the Go side ports it. A workflow that emails
50
+ * a number must not read differently depending on which runner sent it.
51
+ */
52
+ export declare function asText(value: Value): string;
53
+ /**
54
+ * RFC 3339 in UTC, with a fractional part only when there is one.
55
+ *
56
+ * `Date.toISOString()` always writes three decimal places and Go's RFC3339Nano
57
+ * writes none for a whole second, so neither language's default would do. This
58
+ * spelling is Go's, and TypeScript is the one that has to be told.
59
+ *
60
+ * Instants carry millisecond precision, which is what `Date` can hold; the Go
61
+ * side truncates to match rather than quietly keeping nanoseconds one runner
62
+ * would print and the other could not.
63
+ */
64
+ export declare function datetimeToText(value: Date): string;
65
+ /**
66
+ * Canonical JSON — what `json.stringify` produces.
67
+ *
68
+ * Hand-rolled in both languages rather than reaching for the built-in, because
69
+ * the two built-ins disagree in ways that would reach a user: Go sorts object
70
+ * keys and JavaScript preserves insertion order, Go escapes `<` and `&` by
71
+ * default, and each formats numbers its own way. Sorting keys in both is the
72
+ * only choice that can be made identical, so both sort.
73
+ */
74
+ export declare function toJson(value: Value): string;
75
+ /**
76
+ * Compare two strings by code point.
77
+ *
78
+ * JavaScript's `<` compares UTF-16 code units, and Go's compares UTF-8 bytes.
79
+ * Those agree for everything in the basic multilingual plane and disagree above
80
+ * it: a surrogate pair sorts *before* U+E000..U+FFFF in JavaScript and *after*
81
+ * them in Go, so `list.sort(['fi', '😀'])` came out in opposite orders. Go's byte
82
+ * order is already code point order, so this is what TypeScript has to be told.
83
+ *
84
+ * It matters most for `json.stringify`, whose keys are sorted precisely so the
85
+ * two languages produce one string.
86
+ */
87
+ export declare function compareText(left: string, right: string): number;
88
+ /**
89
+ * Half away from zero, which is Go's `math.Round`.
90
+ *
91
+ * `Math.round` rounds halves toward *positive infinity*, so it disagrees on
92
+ * every negative half. Reflecting the negative case and letting `Math.round` do
93
+ * the work is the whole fix; reimplementing rounding is how
94
+ * `sign * floor(abs + 0.5)` got written, which is wrong just below a half.
95
+ *
96
+ * Anything that rounds must come through here. `num.round` and `dt.add` both
97
+ * did their own thing, and only one of them was right.
98
+ */
99
+ export declare function roundHalfAwayFromZero(value: number): number;
100
+ /** ECMAScript `Number::toString`. Named so the Go port has something to point at. */
101
+ export declare function numberToText(value: number): string;
package/package.json CHANGED
@@ -1,17 +1,41 @@
1
1
  {
2
2
  "name": "@hatua/expressions",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving the name. Nothing is published here; see 0.1.0 and later.",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Internal to @hatua/react — published because it is an external of its build, not a supported API. The {{ … }} expression language: parse, check, resolve.",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/pedromvgomes/hatua.git",
9
10
  "directory": "source/packages/expressions"
10
11
  },
12
+ "sideEffects": false,
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "imports": {
23
+ "#generated/*": "./src/generated/*",
24
+ "#nodes": "./src/nodes.ts"
25
+ },
11
26
  "publishConfig": {
12
27
  "access": "public"
13
28
  },
14
- "files": [
15
- "README.md"
16
- ]
29
+ "dependencies": {
30
+ "@hatua/schema": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "yaml": "^2.8.1"
34
+ },
35
+ "scripts": {
36
+ "build": "vite build",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reporter=lcovonly"
40
+ }
17
41
  }
package/README.md DELETED
@@ -1,5 +0,0 @@
1
- # @hatua/expressions
2
-
3
- This version reserves the package name and contains no code.
4
-
5
- Install `0.1.0` or later.