@listen-fire/movement-lang 0.12.0-rc.7 → 0.12.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.
@@ -10,11 +10,13 @@ export interface ConstructionArg {
10
10
  name: string;
11
11
  /**
12
12
  * `credential` = names an imported credential whose `adapter` is this adapter
13
- * (always required). `position` = an entry-position arg (e.g. Sheets'
13
+ * (required unless the adapter works without one: lf's email). `position` = an entry-position arg (e.g. Sheets'
14
14
  * `spreadsheet`) that pins where the instance starts.
15
15
  */
16
16
  kind: 'credential' | 'position';
17
- /** Whether construction MUST supply it. Credential args are required; position args are optional today. */
17
+ /** Whether construction MUST supply it. A credential arg is required unless every credential the
18
+ * adapter declares is optional (an absent one then instantiates the credential-free schema);
19
+ * position args are optional today. */
18
20
  required: boolean;
19
21
  /**
20
22
  * Position args only: the collection/type whose members are the value enum
@@ -12,6 +12,8 @@ export interface RustCheckOptions {
12
12
  * snapshot does not load. */
13
13
  export declare class RustCheckRefused extends Error {
14
14
  constructor(message: string);
15
+ /** The refusal an artifact's `{error: {message}}` reply carries. */
16
+ static of(json: unknown): RustCheckRefused;
15
17
  }
16
18
  /** The language the artifact reads: its release's version. */
17
19
  export declare function languageVersion(): LanguageVersion;
@@ -87,3 +89,7 @@ export declare function compatibleInRust(request: {
87
89
  } | {
88
90
  undescribed: true;
89
91
  };
92
+ /** One diagnostic of an artifact's reply, validated.
93
+ *
94
+ * @internal Used by the story analysis's wrapper (checker/story_analysis.ts). */
95
+ export declare function diagnosticOf(value: unknown): Diagnostic;
@@ -15,6 +15,7 @@ exports.languageStanding = languageStanding;
15
15
  exports.checkSourceInRust = checkSourceInRust;
16
16
  exports.checkAnswerInRust = checkAnswerInRust;
17
17
  exports.compatibleInRust = compatibleInRust;
18
+ exports.diagnosticOf = diagnosticOf;
18
19
  const loader_1 = require("../wasm/loader");
19
20
  const language_version_1 = require("../language_version");
20
21
  /** A source the Rust checker could not check: it does not parse, or the
@@ -24,6 +25,11 @@ class RustCheckRefused extends Error {
24
25
  super(message);
25
26
  this.name = 'RustCheckRefused';
26
27
  }
28
+ /** The refusal an artifact's `{error: {message}}` reply carries. */
29
+ static of(json) {
30
+ const error = isRecord(json) && isRecord(json.error) ? json.error : undefined;
31
+ return new RustCheckRefused(typeof error?.message === 'string' ? error.message : 'the Rust checker refused the check');
32
+ }
27
33
  }
28
34
  exports.RustCheckRefused = RustCheckRefused;
29
35
  /** The language the artifact reads: its release's version. */
@@ -57,7 +63,7 @@ function checkSourceInRust(source, snapshot, options) {
57
63
  const reply = (0, loader_1.callCheckProgram)(source, JSON.stringify(snapshot), callOptions(options));
58
64
  const json = reply.json;
59
65
  if (!reply.ok)
60
- throw refused(json);
66
+ throw RustCheckRefused.of(json);
61
67
  const diagnostics = isRecord(json) ? json.diagnostics : undefined;
62
68
  if (!Array.isArray(diagnostics))
63
69
  throw new Error('the Rust checker answered no diagnostics');
@@ -71,7 +77,7 @@ function checkAnswerInRust(source, snapshot, options) {
71
77
  const reply = (0, loader_1.callCheckAnswer)(source, JSON.stringify(snapshot), callOptions(options));
72
78
  const json = reply.json;
73
79
  if (!reply.ok)
74
- throw refused(json);
80
+ throw RustCheckRefused.of(json);
75
81
  if (!isRecord(json) || !Array.isArray(json.diagnostics) || !Array.isArray(json.stamps)) {
76
82
  throw new Error(`the Rust checker answered no check: ${JSON.stringify(json)?.slice(0, 200)}`);
77
83
  }
@@ -89,7 +95,7 @@ function compatibleInRust(request) {
89
95
  const reply = (0, loader_1.callCheckCompatible)(JSON.stringify(request));
90
96
  const json = reply.json;
91
97
  if (!reply.ok)
92
- throw refused(json);
98
+ throw RustCheckRefused.of(json);
93
99
  if (isRecord(json) && json.undescribed === true)
94
100
  return { undescribed: true };
95
101
  if (isRecord(json) && typeof json.fits === 'boolean')
@@ -99,10 +105,6 @@ function compatibleInRust(request) {
99
105
  function callOptions(options) {
100
106
  return options.upgradingFrom === undefined ? {} : { upgradingFrom: options.upgradingFrom };
101
107
  }
102
- function refused(json) {
103
- const error = isRecord(json) && isRecord(json.error) ? json.error : undefined;
104
- return new RustCheckRefused(typeof error?.message === 'string' ? error.message : 'the Rust checker refused the check');
105
- }
106
108
  function stampOf(value) {
107
109
  if (!isRecord(value) || typeof value.path !== 'string' || (value.file !== undefined && typeof value.file !== 'string')) {
108
110
  throw new Error(`the Rust checker answered a stamp that is not one: ${JSON.stringify(value)}`);
@@ -156,6 +158,9 @@ function linkedKindOf(value) {
156
158
  return value;
157
159
  throw new Error(`the Rust checker answered the import kind ${JSON.stringify(value)}`);
158
160
  }
161
+ /** One diagnostic of an artifact's reply, validated.
162
+ *
163
+ * @internal Used by the story analysis's wrapper (checker/story_analysis.ts). */
159
164
  function diagnosticOf(value) {
160
165
  if (!isRecord(value) || typeof value.code !== 'string' || typeof value.message !== 'string') {
161
166
  throw new Error(`the Rust checker answered a diagnostic that is not one: ${JSON.stringify(value)}`);
@@ -0,0 +1,236 @@
1
+ import type { CollectionOpExpression, CombinatorKind, ExprSlot, FieldEntry, MembersExpression, ShapeNode, Span } from '../parser/ast';
2
+ import type { TraversalStep } from '../expression/types';
3
+ import type { CatalogSnapshot } from '../service/snapshot';
4
+ import type { Diagnostic } from './diagnostic';
5
+ import { type RustCheckOptions } from './rust_checker';
6
+ /** The analysis's shape version this binding writes and reads. */
7
+ export declare const STORY_ANALYSIS_VERSION = 1;
8
+ /** Every analysis version this binding reads; any other is refused by name. */
9
+ export declare const SUPPORTED_STORY_ANALYSIS_VERSIONS: readonly number[];
10
+ export interface StoryAnalysis {
11
+ storyAnalysisVersion: number;
12
+ diagnostics: Diagnostic[];
13
+ /** The file's scope is 0. */
14
+ scopes: StoryScope[];
15
+ symbols: StorySymbol[];
16
+ writes: StoryWrite[];
17
+ nodes: StoryNode[];
18
+ bodies: StoryBody[];
19
+ shapes: StoryShape[];
20
+ }
21
+ /** One scope: the last binding of each name in it, as the walk left it. A
22
+ * scope with no text of its own has no `span`. */
23
+ export interface StoryScope {
24
+ parent?: number;
25
+ span?: Span;
26
+ symbols: Record<string, number>;
27
+ }
28
+ export type StorySymbolKind = 'adapter' | 'credential' | 'plugin' | 'fileImport' | 'binding' | 'instance' | 'param' | 'movement' | 'shape' | 'alias' | 'type';
29
+ /** Where a record is: a system's binding name, and its record type where one place holds it. */
30
+ export interface StoryAddress {
31
+ instance: string;
32
+ position?: string;
33
+ }
34
+ /** What a movement may do. `reads` and `writes` name instance bindings;
35
+ * `partial` is set where a callee or a site could not be placed. */
36
+ export interface StoryEffects {
37
+ reads: string[];
38
+ writes: string[];
39
+ ai: boolean;
40
+ now: boolean;
41
+ suspend: boolean;
42
+ partial: boolean;
43
+ }
44
+ export interface StorySymbol {
45
+ name: string;
46
+ kind: StorySymbolKind;
47
+ span: Span;
48
+ adapter?: string;
49
+ address?: StoryAddress;
50
+ /** The binding holds an extraction's graph; `entity` the node's name, absent at its root. */
51
+ extracted?: {
52
+ entity?: string;
53
+ };
54
+ /** Movements only. */
55
+ effects?: StoryEffects;
56
+ }
57
+ export interface StoryWrite {
58
+ span: Span;
59
+ scope: number;
60
+ /** The field lines, spreads written out (a spread's line carries `spread`). */
61
+ fields: FieldEntry[];
62
+ hasUniqueBy: boolean;
63
+ /** `update` is a write to a position; `find` a match. */
64
+ action: 'create' | 'update' | 'find';
65
+ parents: Array<{
66
+ edge: string;
67
+ type?: string;
68
+ }>;
69
+ binding?: string;
70
+ target?: {
71
+ instance: string;
72
+ recordType?: string;
73
+ };
74
+ }
75
+ /** Every node has its `span` and the `scope` it sits in. */
76
+ export type StoryNode = {
77
+ span: Span;
78
+ scope: number;
79
+ } & ({
80
+ kind: 'instance';
81
+ name: string;
82
+ adapterType?: string;
83
+ } | {
84
+ kind: 'listen';
85
+ instance: string;
86
+ adapterType?: string;
87
+ fires: string;
88
+ firesMovement: boolean;
89
+ events?: string[];
90
+ narrowing: Record<string, string>;
91
+ eventTypes: Array<{
92
+ key: string;
93
+ display?: string;
94
+ }>;
95
+ } | {
96
+ kind: 'link';
97
+ from: string;
98
+ edge: string;
99
+ to: {
100
+ kind: 'handle';
101
+ name: string;
102
+ } | {
103
+ kind: 'match';
104
+ span: Span;
105
+ };
106
+ } | {
107
+ kind: 'unlink';
108
+ from: string;
109
+ edge: string;
110
+ to: {
111
+ kind: 'handle';
112
+ name: string;
113
+ };
114
+ } | {
115
+ kind: 'await';
116
+ binding?: string;
117
+ source: StoryAwaitedSource;
118
+ } | {
119
+ kind: 'combinator';
120
+ combinator: CombinatorKind;
121
+ binding?: string;
122
+ arms: Array<{
123
+ span: Span;
124
+ scope?: number;
125
+ }>;
126
+ } | {
127
+ kind: 'branch';
128
+ arms: Array<{
129
+ span: Span;
130
+ scope: number;
131
+ conditionSpan: Span;
132
+ }>;
133
+ otherwise?: {
134
+ span: Span;
135
+ scope: number;
136
+ };
137
+ } | {
138
+ kind: 'extract';
139
+ binding?: string;
140
+ node?: StoryExtractNode;
141
+ }
142
+ /** `argParams[i]` is null for an argument past the callee's last parameter. */
143
+ | {
144
+ kind: 'call';
145
+ binding?: string;
146
+ callee: string;
147
+ isMovement: boolean;
148
+ argParams?: Array<string | null>;
149
+ } | {
150
+ kind: 'callReading';
151
+ reading: StoryCallReading;
152
+ } | (StoryWalk & {
153
+ kind: 'traversal';
154
+ from?: 'graph' | 'result';
155
+ })
156
+ /** `key` is the walk's comparable token (its root and steps as JSON, keys sorted). */
157
+ | (StoryWalk & {
158
+ kind: 'valuePath';
159
+ key: string;
160
+ }) | {
161
+ kind: 'delete';
162
+ subject: string;
163
+ } | {
164
+ kind: 'refresh';
165
+ subject: string;
166
+ });
167
+ export type StoryNodeKind = StoryNode['kind'];
168
+ /** A walk's root, its hops, and where each hop landed (null where it did not type). */
169
+ export interface StoryWalk {
170
+ root?: string;
171
+ steps: TraversalStep[];
172
+ landings: Array<StoryAddress | null>;
173
+ }
174
+ export type StoryAwaitedSource = {
175
+ kind: 'traversal';
176
+ root?: string;
177
+ edge?: string;
178
+ awaitable?: boolean;
179
+ resolvesEmpty?: boolean;
180
+ } | {
181
+ kind: 'sleep';
182
+ duration: string;
183
+ } | {
184
+ kind: 'until';
185
+ every?: string;
186
+ };
187
+ export type StoryCallReading = {
188
+ kind: 'value';
189
+ expr: ExprSlot;
190
+ } | {
191
+ kind: 'collection';
192
+ collection: CollectionOpExpression;
193
+ } | {
194
+ kind: 'members';
195
+ members: MembersExpression;
196
+ } | {
197
+ kind: 'refused';
198
+ refusal: {
199
+ code: string;
200
+ message: string;
201
+ span: Span;
202
+ };
203
+ };
204
+ /** An extraction's graph: each field's `type` in the author's words. */
205
+ export interface StoryExtractNode {
206
+ name: string;
207
+ fields: Array<{
208
+ name: string;
209
+ at: Span;
210
+ type?: string;
211
+ }>;
212
+ children: StoryExtractNode[];
213
+ }
214
+ /** A statement list the story caps a lane by, and whether it leaves. */
215
+ export interface StoryBody {
216
+ span: Span;
217
+ kind: 'movement' | 'arm' | 'else' | 'combinatorArm' | 'callback';
218
+ terminates: boolean;
219
+ }
220
+ /** A file-level node declaration, `extends` folded into `root`. */
221
+ export interface StoryShape {
222
+ name: string;
223
+ span: Span;
224
+ root: ShapeNode;
225
+ }
226
+ /**
227
+ * The story analysis of `source` against `snapshot`. A pin this release
228
+ * does not read is its version diagnostic alone, every list empty, the
229
+ * artifact not asked (`refused_story_analysis` in Rust). Throws
230
+ * `RustCheckRefused` for a source that does not parse or a snapshot that
231
+ * does not load, and for an analysis version this binding does not read.
232
+ */
233
+ export declare function storyAnalysisInRust(source: string, snapshot: CatalogSnapshot, options: RustCheckOptions): StoryAnalysis;
234
+ /** `name` from scope `scope`, through the table: the last binding of the
235
+ * name in the nearest enclosing scope that binds it. */
236
+ export declare function resolveInAnalysis(analysis: StoryAnalysis, scope: number, name: string): StorySymbol | undefined;
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ // The story analysis (crates/movement-checker/src/story.rs), through the
3
+ // movement artifact's `story_analysis` export: what the one check walk
4
+ // resolved, as data a story projection reads. A scope is an id into
5
+ // `scopes`, a symbol an id into `symbols`; spans are the AST's (1-based).
6
+ // The diagnostics are the check's own, so one call gives both the verdict
7
+ // and the picture.
8
+ //
9
+ // The envelope and every record's identity (its kind, span and scope) are
10
+ // validated here. What a record carries from the AST (a write's fields, a
11
+ // walk's steps, a node declaration's tree) is trusted as the Rust's own
12
+ // output, as the parser's tree is (parser/rust_parser.ts).
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.SUPPORTED_STORY_ANALYSIS_VERSIONS = exports.STORY_ANALYSIS_VERSION = void 0;
15
+ exports.storyAnalysisInRust = storyAnalysisInRust;
16
+ exports.resolveInAnalysis = resolveInAnalysis;
17
+ const loader_1 = require("../wasm/loader");
18
+ const language_version_1 = require("../language_version");
19
+ const rust_checker_1 = require("./rust_checker");
20
+ /** The analysis's shape version this binding writes and reads. */
21
+ exports.STORY_ANALYSIS_VERSION = 1;
22
+ /** Every analysis version this binding reads; any other is refused by name. */
23
+ exports.SUPPORTED_STORY_ANALYSIS_VERSIONS = [exports.STORY_ANALYSIS_VERSION];
24
+ /**
25
+ * The story analysis of `source` against `snapshot`. A pin this release
26
+ * does not read is its version diagnostic alone, every list empty, the
27
+ * artifact not asked (`refused_story_analysis` in Rust). Throws
28
+ * `RustCheckRefused` for a source that does not parse or a snapshot that
29
+ * does not load, and for an analysis version this binding does not read.
30
+ */
31
+ function storyAnalysisInRust(source, snapshot, options) {
32
+ const refusal = (0, language_version_1.languageVersionDiagnostic)(options.languageVersion);
33
+ if (refusal !== undefined) {
34
+ return {
35
+ storyAnalysisVersion: exports.STORY_ANALYSIS_VERSION,
36
+ diagnostics: [refusal],
37
+ scopes: [],
38
+ symbols: [],
39
+ writes: [],
40
+ nodes: [],
41
+ bodies: [],
42
+ shapes: [],
43
+ };
44
+ }
45
+ const reply = (0, loader_1.callStoryAnalysis)(source, JSON.stringify(snapshot), options.upgradingFrom === undefined ? {} : { upgradingFrom: options.upgradingFrom });
46
+ const json = reply.json;
47
+ if (!reply.ok)
48
+ throw rust_checker_1.RustCheckRefused.of(json);
49
+ return analysisOf(json);
50
+ }
51
+ /** `name` from scope `scope`, through the table: the last binding of the
52
+ * name in the nearest enclosing scope that binds it. */
53
+ function resolveInAnalysis(analysis, scope, name) {
54
+ for (let at = scope; at !== undefined; at = analysis.scopes[at]?.parent) {
55
+ const bound = analysis.scopes[at]?.symbols[name];
56
+ if (bound !== undefined)
57
+ return analysis.symbols[bound];
58
+ }
59
+ return undefined;
60
+ }
61
+ function analysisOf(json) {
62
+ if (!isRecord(json))
63
+ throw malformed('analysis', json);
64
+ const version = json.storyAnalysisVersion;
65
+ if (typeof version !== 'number' || !exports.SUPPORTED_STORY_ANALYSIS_VERSIONS.includes(version)) {
66
+ throw new rust_checker_1.RustCheckRefused(`the story analysis is version ${JSON.stringify(version)}, which this binding does not read (it reads ${exports.SUPPORTED_STORY_ANALYSIS_VERSIONS.join(', ')})`);
67
+ }
68
+ const { diagnostics, scopes, symbols, writes, nodes, bodies, shapes } = json;
69
+ if (!Array.isArray(diagnostics) ||
70
+ !Array.isArray(scopes) ||
71
+ !Array.isArray(symbols) ||
72
+ !Array.isArray(writes) ||
73
+ !Array.isArray(nodes) ||
74
+ !Array.isArray(bodies) ||
75
+ !Array.isArray(shapes)) {
76
+ throw malformed('analysis', json);
77
+ }
78
+ return {
79
+ storyAnalysisVersion: version,
80
+ diagnostics: diagnostics.map(rust_checker_1.diagnosticOf),
81
+ scopes: scopes.map(each(isScope, 'scope')),
82
+ symbols: symbols.map(each(isSymbol, 'symbol')),
83
+ writes: writes.map(each(isWrite, 'write')),
84
+ nodes: nodes.map(each(isNode, 'node')),
85
+ bodies: bodies.map(each(isBody, 'body')),
86
+ shapes: shapes.map(each(isShape, 'shape')),
87
+ };
88
+ }
89
+ function each(is, what) {
90
+ return value => {
91
+ if (!is(value))
92
+ throw malformed(what, value);
93
+ return value;
94
+ };
95
+ }
96
+ const SYMBOL_KINDS = new Set([
97
+ 'adapter',
98
+ 'credential',
99
+ 'plugin',
100
+ 'fileImport',
101
+ 'binding',
102
+ 'instance',
103
+ 'param',
104
+ 'movement',
105
+ 'shape',
106
+ 'alias',
107
+ 'type',
108
+ ]);
109
+ const NODE_KINDS = new Set([
110
+ 'instance',
111
+ 'listen',
112
+ 'link',
113
+ 'unlink',
114
+ 'await',
115
+ 'combinator',
116
+ 'branch',
117
+ 'extract',
118
+ 'call',
119
+ 'callReading',
120
+ 'traversal',
121
+ 'valuePath',
122
+ 'delete',
123
+ 'refresh',
124
+ ]);
125
+ const BODY_KINDS = new Set(['movement', 'arm', 'else', 'combinatorArm', 'callback']);
126
+ function isScope(value) {
127
+ return (isRecord(value) &&
128
+ (value.parent === undefined || isId(value.parent)) &&
129
+ (value.span === undefined || isSpan(value.span)) &&
130
+ isRecord(value.symbols) &&
131
+ Object.values(value.symbols).every(isId));
132
+ }
133
+ function isSymbol(value) {
134
+ return isRecord(value) && typeof value.name === 'string' && typeof value.kind === 'string' && SYMBOL_KINDS.has(value.kind) && isSpan(value.span);
135
+ }
136
+ function isWrite(value) {
137
+ return (isRecord(value) &&
138
+ isSpan(value.span) &&
139
+ isId(value.scope) &&
140
+ Array.isArray(value.fields) &&
141
+ typeof value.hasUniqueBy === 'boolean' &&
142
+ (value.action === 'create' || value.action === 'update' || value.action === 'find') &&
143
+ Array.isArray(value.parents));
144
+ }
145
+ function isNode(value) {
146
+ return isRecord(value) && typeof value.kind === 'string' && NODE_KINDS.has(value.kind) && isSpan(value.span) && isId(value.scope);
147
+ }
148
+ function isBody(value) {
149
+ return isRecord(value) && isSpan(value.span) && typeof value.kind === 'string' && BODY_KINDS.has(value.kind) && typeof value.terminates === 'boolean';
150
+ }
151
+ function isShape(value) {
152
+ return isRecord(value) && typeof value.name === 'string' && isSpan(value.span) && isRecord(value.root);
153
+ }
154
+ function isId(value) {
155
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
156
+ }
157
+ function isSpan(value) {
158
+ return isRecord(value) && isLoc(value.start) && isLoc(value.end);
159
+ }
160
+ function isLoc(value) {
161
+ return isRecord(value) && typeof value.line === 'number' && typeof value.col === 'number';
162
+ }
163
+ function isRecord(value) {
164
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
165
+ }
166
+ function malformed(what, value) {
167
+ return new Error(`the story analysis answered a ${what} that is not one: ${JSON.stringify(value)?.slice(0, 200)}`);
168
+ }
package/dist/index.d.ts CHANGED
@@ -13,7 +13,9 @@ export * from './checker/fixes';
13
13
  export * from './checker/diagnostic';
14
14
  export * from './checker/linked_types';
15
15
  export * from './checker/rust_checker';
16
+ export * from './checker/story_analysis';
16
17
  export * from './checker/standard_library';
17
18
  export type { MExpr } from './parser/expression/tree';
18
19
  export * from './service/snapshot';
20
+ export * from './service/language_service';
19
21
  export * from './language_version';
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ __exportStar(require("./checker/fixes"), exports);
36
36
  __exportStar(require("./checker/diagnostic"), exports);
37
37
  __exportStar(require("./checker/linked_types"), exports);
38
38
  __exportStar(require("./checker/rust_checker"), exports);
39
+ __exportStar(require("./checker/story_analysis"), exports);
39
40
  __exportStar(require("./checker/standard_library"), exports);
40
41
  __exportStar(require("./service/snapshot"), exports);
42
+ __exportStar(require("./service/language_service"), exports);
41
43
  __exportStar(require("./language_version"), exports);
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.12.0-rc.7";
1
+ export declare const PACKAGE_VERSION = "0.12.0";
@@ -5,4 +5,4 @@ exports.PACKAGE_VERSION = void 0;
5
5
  // scripts/build-packages.cjs (a source and its build sit at different depths
6
6
  // beside package.json, so neither can require it by one relative path).
7
7
  // __test__/language_version.unit.test.ts holds it equal to package.json.
8
- exports.PACKAGE_VERSION = '0.12.0-rc.7';
8
+ exports.PACKAGE_VERSION = '0.12.0';
@@ -0,0 +1,82 @@
1
+ import { type LanguageVersion } from '../language_version';
2
+ import type { CatalogSnapshot } from './snapshot';
3
+ export interface LanguageServiceOptions {
4
+ /** The program's pin. A pin this release does not read gets empty answers,
5
+ * the artifact not asked. */
6
+ languageVersion: LanguageVersion;
7
+ }
8
+ /** A 0-based line and a UTF-16 column within it, as the wasm counts. */
9
+ export interface LinePosition {
10
+ line: number;
11
+ character: number;
12
+ }
13
+ export type CompletionKind = 'variable' | 'function' | 'field' | 'edge' | 'module' | 'keyword' | 'property' | 'credential' | 'type' | 'namespace';
14
+ /** An entry that opens a connection flow rather than inserting text: the
15
+ * adapter's snapshot entry declares it (`connectActions`). */
16
+ export interface CompletionConnectAction {
17
+ kind: string;
18
+ adapter: string;
19
+ credential?: string;
20
+ }
21
+ export interface CompletionItem {
22
+ label: string;
23
+ kind: CompletionKind;
24
+ detail?: string;
25
+ /** The text to insert, where it is not the label. */
26
+ insert?: string;
27
+ /** What the item replaces: its own range, or the word before the cursor. */
28
+ from: number;
29
+ to: number;
30
+ connectAction?: CompletionConnectAction;
31
+ }
32
+ export interface Completions {
33
+ /** The word before the cursor, which an item replaces unless it says otherwise. */
34
+ from: number;
35
+ to: number;
36
+ items: CompletionItem[];
37
+ }
38
+ export interface HoverCard {
39
+ from: number;
40
+ to: number;
41
+ markdown: string;
42
+ }
43
+ export type ModuleNamespace = 'adapters' | 'credentials' | 'plugins';
44
+ /** Where a name is defined: this file (`file: null`), a library file of the
45
+ * snapshot (offsets into its text), or a module the name was imported from. */
46
+ export type Definition = {
47
+ file: null;
48
+ from: number;
49
+ to: number;
50
+ } | {
51
+ file: string;
52
+ from: number;
53
+ to: number;
54
+ } | {
55
+ module: {
56
+ namespace: ModuleNamespace;
57
+ name: string;
58
+ };
59
+ };
60
+ export interface QuickFix {
61
+ title: string;
62
+ /** The diagnostic code the fix answers. */
63
+ code: string;
64
+ preferred: boolean;
65
+ edits: Array<{
66
+ from: number;
67
+ to: number;
68
+ insert: string;
69
+ }>;
70
+ }
71
+ /** The completions at `offset`. */
72
+ export declare function completionsAt(source: string, offset: number, snapshot: CatalogSnapshot, options: LanguageServiceOptions): Completions;
73
+ /** The card for what is under `offset`, or null where there is none. */
74
+ export declare function hoverAt(source: string, offset: number, snapshot: CatalogSnapshot, options: LanguageServiceOptions): HoverCard | null;
75
+ /** Where the name under `offset` is defined, or null. */
76
+ export declare function definitionAt(source: string, offset: number, snapshot: CatalogSnapshot, options: LanguageServiceOptions): Definition | null;
77
+ /** The quick fixes for the diagnostics touching `from`..`to`. */
78
+ export declare function quickFixesIn(source: string, from: number, to: number, snapshot: CatalogSnapshot, options: LanguageServiceOptions): QuickFix[];
79
+ /** The UTF-16 offset of `position` in `text`, clamped to its line and to the text. */
80
+ export declare function offsetOf(text: string, position: LinePosition): number;
81
+ /** The line and UTF-16 column of `offset` in `text`, `offset` clamped to the text. */
82
+ export declare function positionOf(text: string, offset: number): LinePosition;
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ // The language service (crates/movement-language), through the movement
3
+ // artifact's `language_query` export: completion, hover, go-to-definition
4
+ // and quick fixes for a program in this release's language, answered as the
5
+ // native service answers `lf lsp`.
6
+ //
7
+ // Positions are UTF-16 offsets into the source string (CodeMirror's unit,
8
+ // and a JavaScript string's own); the wasm counts 0-based lines and UTF-16
9
+ // columns, split on `\n` as the native service splits them. A definition in
10
+ // a library file is an offset into that file's text in `snapshot.files`.
11
+ //
12
+ // It loads with the engine artifact, which a browser never downloads: the
13
+ // editor asks it through the server, as it asks for the check.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.completionsAt = completionsAt;
16
+ exports.hoverAt = hoverAt;
17
+ exports.definitionAt = definitionAt;
18
+ exports.quickFixesIn = quickFixesIn;
19
+ exports.offsetOf = offsetOf;
20
+ exports.positionOf = positionOf;
21
+ const loader_1 = require("../wasm/loader");
22
+ const language_version_1 = require("../language_version");
23
+ const rust_checker_1 = require("../checker/rust_checker");
24
+ /** The completions at `offset`. */
25
+ function completionsAt(source, offset, snapshot, options) {
26
+ if (unread(options))
27
+ return { from: offset, to: offset, items: [] };
28
+ const answer = ask(source, snapshot, { kind: 'complete', position: positionOf(source, offset) });
29
+ if (!isRecord(answer) || !Array.isArray(answer.items))
30
+ throw malformed('completion', answer);
31
+ const word = rangeOf(source, answer.word);
32
+ return { ...word, items: answer.items.map(item => itemOf(source, item, word)) };
33
+ }
34
+ /** The card for what is under `offset`, or null where there is none. */
35
+ function hoverAt(source, offset, snapshot, options) {
36
+ if (unread(options))
37
+ return null;
38
+ const answer = ask(source, snapshot, { kind: 'hover', position: positionOf(source, offset) });
39
+ if (answer === null)
40
+ return null;
41
+ if (!isRecord(answer) || typeof answer.contents !== 'string')
42
+ throw malformed('hover', answer);
43
+ return { ...rangeOf(source, answer.range), markdown: answer.contents };
44
+ }
45
+ /** Where the name under `offset` is defined, or null. */
46
+ function definitionAt(source, offset, snapshot, options) {
47
+ if (unread(options))
48
+ return null;
49
+ const answer = ask(source, snapshot, { kind: 'definition', position: positionOf(source, offset) });
50
+ if (answer === null)
51
+ return null;
52
+ if (!isRecord(answer) || (answer.file !== null && typeof answer.file !== 'string'))
53
+ throw malformed('definition', answer);
54
+ if (answer.module !== undefined)
55
+ return { module: moduleOf(answer.module) };
56
+ if (answer.file === null)
57
+ return { file: null, ...rangeOf(source, answer.range) };
58
+ // A library file the snapshot does not carry has no text: its offsets clamp to 0.
59
+ const text = snapshot.files?.[answer.file]?.source ?? '';
60
+ return { file: answer.file, ...rangeOf(text, answer.range) };
61
+ }
62
+ /** The quick fixes for the diagnostics touching `from`..`to`. */
63
+ function quickFixesIn(source, from, to, snapshot, options) {
64
+ if (unread(options))
65
+ return [];
66
+ const range = { start: positionOf(source, from), end: positionOf(source, to) };
67
+ const answer = ask(source, snapshot, { kind: 'codeActions', position: range.start, range });
68
+ if (!Array.isArray(answer))
69
+ throw malformed('code actions', answer);
70
+ return answer.map(action => {
71
+ if (!isRecord(action) ||
72
+ typeof action.title !== 'string' ||
73
+ typeof action.code !== 'string' ||
74
+ typeof action.preferred !== 'boolean' ||
75
+ !Array.isArray(action.edits)) {
76
+ throw malformed('code action', action);
77
+ }
78
+ return {
79
+ title: action.title,
80
+ code: action.code,
81
+ preferred: action.preferred,
82
+ edits: action.edits.map(edit => {
83
+ if (!isRecord(edit) || typeof edit.newText !== 'string')
84
+ throw malformed('edit', edit);
85
+ return { ...rangeOf(source, edit.range), insert: edit.newText };
86
+ }),
87
+ };
88
+ });
89
+ }
90
+ /** The UTF-16 offset of `position` in `text`, clamped to its line and to the text. */
91
+ function offsetOf(text, position) {
92
+ let start = 0;
93
+ for (let line = 0; line < position.line; line++) {
94
+ const newline = text.indexOf('\n', start);
95
+ if (newline === -1)
96
+ return text.length;
97
+ start = newline + 1;
98
+ }
99
+ const newline = text.indexOf('\n', start);
100
+ const end = newline === -1 ? text.length : newline;
101
+ return Math.min(start + position.character, end);
102
+ }
103
+ /** The line and UTF-16 column of `offset` in `text`, `offset` clamped to the text. */
104
+ function positionOf(text, offset) {
105
+ const at = Math.max(0, Math.min(offset, text.length));
106
+ let line = 0;
107
+ let start = 0;
108
+ for (let newline = text.indexOf('\n'); newline !== -1 && newline < at; newline = text.indexOf('\n', start)) {
109
+ line += 1;
110
+ start = newline + 1;
111
+ }
112
+ return { line, character: at - start };
113
+ }
114
+ function unread(options) {
115
+ return (0, language_version_1.languageVersionDiagnostic)(options.languageVersion) !== undefined;
116
+ }
117
+ function ask(source, snapshot, query) {
118
+ const reply = (0, loader_1.callLanguageQuery)(source, JSON.stringify(snapshot), JSON.stringify(query));
119
+ if (!reply.ok)
120
+ throw rust_checker_1.RustCheckRefused.of(reply.json);
121
+ return reply.json;
122
+ }
123
+ const KINDS = new Set([
124
+ 'variable',
125
+ 'function',
126
+ 'field',
127
+ 'edge',
128
+ 'module',
129
+ 'keyword',
130
+ 'property',
131
+ 'credential',
132
+ 'type',
133
+ 'namespace',
134
+ ]);
135
+ function isKind(value) {
136
+ return typeof value === 'string' && KINDS.has(value);
137
+ }
138
+ function itemOf(source, value, word) {
139
+ if (!isRecord(value) || typeof value.label !== 'string' || !isKind(value.kind))
140
+ throw malformed('completion item', value);
141
+ const { label, kind, detail, insert, replace, connectAction } = value;
142
+ if ((detail !== undefined && typeof detail !== 'string') || (insert !== undefined && typeof insert !== 'string')) {
143
+ throw malformed('completion item', value);
144
+ }
145
+ return {
146
+ label,
147
+ kind,
148
+ ...(detail !== undefined ? { detail } : {}),
149
+ ...(insert !== undefined ? { insert } : {}),
150
+ ...(replace !== undefined ? rangeOf(source, replace) : word),
151
+ ...(connectAction !== undefined ? { connectAction: connectActionOf(connectAction) } : {}),
152
+ };
153
+ }
154
+ function connectActionOf(value) {
155
+ if (!isRecord(value) ||
156
+ typeof value.kind !== 'string' ||
157
+ typeof value.adapter !== 'string' ||
158
+ (value.credential !== undefined && typeof value.credential !== 'string')) {
159
+ throw malformed('connect action', value);
160
+ }
161
+ return { kind: value.kind, adapter: value.adapter, ...(value.credential !== undefined ? { credential: value.credential } : {}) };
162
+ }
163
+ function moduleOf(value) {
164
+ if (!isRecord(value) || typeof value.name !== 'string')
165
+ throw malformed('module', value);
166
+ const { namespace, name } = value;
167
+ if (namespace !== 'adapters' && namespace !== 'credentials' && namespace !== 'plugins')
168
+ throw malformed('module', value);
169
+ return { namespace, name };
170
+ }
171
+ function rangeOf(text, value) {
172
+ if (!isRecord(value))
173
+ throw malformed('range', value);
174
+ return { from: offsetOf(text, linePositionOf(value.start)), to: offsetOf(text, linePositionOf(value.end)) };
175
+ }
176
+ function linePositionOf(value) {
177
+ if (!isRecord(value) || typeof value.line !== 'number' || typeof value.character !== 'number')
178
+ throw malformed('position', value);
179
+ return { line: value.line, character: value.character };
180
+ }
181
+ function isRecord(value) {
182
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
183
+ }
184
+ function malformed(what, value) {
185
+ return new Error(`the language service answered a ${what} that is not one: ${JSON.stringify(value)?.slice(0, 200)}`);
186
+ }
@@ -190,6 +190,10 @@ function fromCatalogSnapshot(snapshot) {
190
190
  // without pre-processing (defense-in-depth: callers should unwrap first,
191
191
  // but the snapshot instantiate is the last lookup that can catch it).
192
192
  const rawCredName = args?.[credArg?.name ?? 'credentials']?.trim();
193
+ // A credential the adapter can do without, not given: the
194
+ // credential-free instance (lf's email, which receives with none).
195
+ if (rawCredName === undefined && credArg !== undefined && !credArg.required)
196
+ return at();
193
197
  const credentialName = rawCredName !== undefined
194
198
  ? ((0, scan_1.unwrapCredentialArg)(rawCredName) ?? rawCredName)
195
199
  : rawCredName;
@@ -128,6 +128,31 @@ export declare function callLanguageStanding(pin: string): {
128
128
  ok: boolean;
129
129
  json: unknown;
130
130
  };
131
+ /**
132
+ * The language service (crates/movement-language/src/abi.rs
133
+ * `language_query`): the source, the snapshot and the query as JSON, and the
134
+ * reply — the native service's answer (an object, an array or `null`), or
135
+ * `{error}` for a snapshot or a query that does not read.
136
+ *
137
+ * @internal Used by the service's wrapper (service/language_service.ts).
138
+ */
139
+ export declare function callLanguageQuery(source: string, snapshotJson: string, queryJson: string): {
140
+ ok: boolean;
141
+ json: unknown;
142
+ };
143
+ /**
144
+ * The checker's story analysis (crates/movement-checker/src/story.rs
145
+ * `story_analysis`): `callCheckProgram`'s inputs, and the reply —
146
+ * `{storyAnalysisVersion, diagnostics, scopes, symbols, writes, nodes,
147
+ * bodies, shapes}`, or `{error}` for a source that does not parse or a
148
+ * snapshot that does not load.
149
+ *
150
+ * @internal Used by the analysis's wrapper (checker/story_analysis.ts).
151
+ */
152
+ export declare function callStoryAnalysis(source: string, snapshotJson: string, options: CheckCallOptions): {
153
+ ok: boolean;
154
+ json: unknown;
155
+ };
131
156
  /**
132
157
  * One engine segment's conversation (crates/movement-engine/src/abi.rs):
133
158
  * `start` and `answer` each return the next step as `{ok: true, json}` —
@@ -34,6 +34,8 @@ exports.callCheckCompatible = callCheckCompatible;
34
34
  exports.callStandardLibrary = callStandardLibrary;
35
35
  exports.callLanguageVersion = callLanguageVersion;
36
36
  exports.callLanguageStanding = callLanguageStanding;
37
+ exports.callLanguageQuery = callLanguageQuery;
38
+ exports.callStoryAnalysis = callStoryAnalysis;
37
39
  exports.initEngine = initEngine;
38
40
  exports.openEngineSegment = openEngineSegment;
39
41
  exports.parserGeneration = parserGeneration;
@@ -185,6 +187,29 @@ function callLanguageVersion() {
185
187
  function callLanguageStanding(pin) {
186
188
  return callChecker('language_standing', [pin], []);
187
189
  }
190
+ /**
191
+ * The language service (crates/movement-language/src/abi.rs
192
+ * `language_query`): the source, the snapshot and the query as JSON, and the
193
+ * reply — the native service's answer (an object, an array or `null`), or
194
+ * `{error}` for a snapshot or a query that does not read.
195
+ *
196
+ * @internal Used by the service's wrapper (service/language_service.ts).
197
+ */
198
+ function callLanguageQuery(source, snapshotJson, queryJson) {
199
+ return callChecker('language_query', [source, snapshotJson, queryJson], []);
200
+ }
201
+ /**
202
+ * The checker's story analysis (crates/movement-checker/src/story.rs
203
+ * `story_analysis`): `callCheckProgram`'s inputs, and the reply —
204
+ * `{storyAnalysisVersion, diagnostics, scopes, symbols, writes, nodes,
205
+ * bodies, shapes}`, or `{error}` for a source that does not parse or a
206
+ * snapshot that does not load.
207
+ *
208
+ * @internal Used by the analysis's wrapper (checker/story_analysis.ts).
209
+ */
210
+ function callStoryAnalysis(source, snapshotJson, options) {
211
+ return callChecker('story_analysis', [source, snapshotJson, optionsJson(options)], []);
212
+ }
188
213
  function callChecker(fn, inputs, extra) {
189
214
  const exports = currentExports();
190
215
  const checker = exports.checker[fn];
@@ -400,7 +425,16 @@ function readExports(raw) {
400
425
  catalog_query: exported('catalog_query'),
401
426
  };
402
427
  const checker = {};
403
- for (const fn of ['check_program', 'check_answer', 'check_compatible', 'standard_library', 'language_version', 'language_standing']) {
428
+ for (const fn of [
429
+ 'check_program',
430
+ 'check_answer',
431
+ 'check_compatible',
432
+ 'standard_library',
433
+ 'language_version',
434
+ 'language_standing',
435
+ 'language_query',
436
+ 'story_analysis',
437
+ ]) {
404
438
  if (typeof raw[fn] !== 'function')
405
439
  continue;
406
440
  const call = exported(fn);
Binary file
Binary file
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@listen-fire/movement-lang",
3
3
  "license": "Apache-2.0",
4
- "version": "0.12.0-rc.7",
4
+ "version": "0.12.0",
5
5
  "description": "The data-movement language: the wasm parser, checker and engine with their loader and bindings, the version rule, the expression AST and the types the contracts name",
6
6
  "engines": {
7
- "node": "22.20.0"
7
+ "node": ">=22.23.4 <23"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -26,8 +26,7 @@
26
26
  "dist"
27
27
  ],
28
28
  "publishConfig": {
29
- "access": "public",
30
- "tag": "next"
29
+ "access": "public"
31
30
  },
32
31
  "scripts": {
33
32
  "build": "tsc --project tsconfig.build.json && cp wasm/*.wasm dist/wasm/",