@rsconcept/domain 1.7.7 → 1.8.1
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/dist/{analyzer-DqjdciC5.d.ts → analyzer-iqAXyROE.d.ts} +2 -3
- package/dist/{arguments-extractor-Dd1lFom9.d.ts → arguments-extractor-ngatANBI.d.ts} +2 -2
- package/dist/{ast-annotations-CZCzs9Uk.d.ts → ast-annotations-D6KZDu12.d.ts} +2 -2
- package/dist/{calculator-Dt03dsnd.d.ts → calculator-k8BGIKmj.d.ts} +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/library/index.d.ts +97 -1
- package/dist/library/index.js +283 -1
- package/dist/library/index.js.map +1 -0
- package/dist/library/oss-api.js +1 -1
- package/dist/library/rsengine.d.ts +1 -1
- package/dist/library/rsform-api.d.ts +13 -3
- package/dist/library/rsform-api.js +24 -2
- package/dist/library/rsform-api.js.map +1 -1
- package/dist/library/rsform.d.ts +2 -2
- package/dist/library/rsmodel-api.d.ts +1 -1
- package/dist/library/structure-planner.d.ts +1 -1
- package/dist/library/structure-planner.js +1 -1
- package/dist/rslang/api.d.ts +13 -2
- package/dist/rslang/api.js +31 -2
- package/dist/rslang/api.js.map +1 -1
- package/dist/rslang/ast-annotations.d.ts +1 -1
- package/dist/rslang/eval/calculator.d.ts +1 -1
- package/dist/rslang/eval/value-api.d.ts +1 -1
- package/dist/rslang/index.d.ts +8 -8
- package/dist/rslang/labels.d.ts +4 -2
- package/dist/rslang/labels.js +8 -2
- package/dist/rslang/labels.js.map +1 -1
- package/dist/rslang/parser/syntax-errors/index.d.ts +1 -1
- package/dist/rslang/semantic/analyzer.d.ts +1 -1
- package/dist/rslang/semantic/analyzer.js +5 -13
- package/dist/rslang/semantic/analyzer.js.map +1 -1
- package/dist/rslang/semantic/arguments-extractor.d.ts +1 -1
- package/dist/rslang/semantic/type-auditor.d.ts +1 -1
- package/dist/rslang/semantic/type-auditor.js +2 -2
- package/dist/rslang/semantic/type-auditor.js.map +1 -1
- package/dist/rslang/semantic/typification-api.d.ts +1 -1
- package/dist/rslang/semantic/typification-parser.d.ts +1 -1
- package/dist/rslang/semantic/typification.d.ts +2 -2
- package/dist/rslang/semantic/typification.js +14 -6
- package/dist/rslang/semantic/typification.js.map +1 -1
- package/dist/rslang/typification-graph.d.ts +1 -1
- package/dist/{typification-DDuv88ir.d.ts → typification-9lNu9R4k.d.ts} +8 -4
- package/dist/{typification-graph-8VdK6_Fh.d.ts → typification-graph-BY_oGT82.d.ts} +2 -2
- package/dist/{typification-parser-DHO42ZRc.d.ts → typification-parser-Dytsv9CI.d.ts} +2 -2
- package/dist/{value-api-C2X4Pom5.d.ts → value-api-CXLl_zg0.d.ts} +2 -2
- package/package.json +1 -1
- package/src/library/index.ts +8 -0
- package/src/library/rsform-api.test.ts +11 -1
- package/src/library/rsform-api.ts +40 -1
- package/src/library/template-instantiation.test.ts +427 -0
- package/src/library/template-instantiation.ts +456 -0
- package/src/rslang/api.test.ts +28 -0
- package/src/rslang/api.ts +32 -2
- package/src/rslang/labels.ts +10 -1
- package/src/rslang/semantic/analyzer.ts +6 -14
- package/src/rslang/semantic/type-auditor.test.ts +25 -6
- package/src/rslang/semantic/type-auditor.ts +2 -1
- package/src/rslang/semantic/typification-api.test.ts +21 -5
- package/src/rslang/semantic/typification-parser.test.ts +6 -1
- package/src/rslang/semantic/typification.ts +16 -6
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module: template instantiation from the expression bank into a target {@link RSForm}.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type AstNode, visitAstDFS } from '../parsing';
|
|
6
|
+
import {
|
|
7
|
+
type AliasMapping,
|
|
8
|
+
applyAliasMapping,
|
|
9
|
+
isGlobalAlias,
|
|
10
|
+
parseRSLangExpression,
|
|
11
|
+
splitTemplateDefinition,
|
|
12
|
+
substituteLocalAliases,
|
|
13
|
+
substituteTemplateArgs
|
|
14
|
+
} from '../rslang/api';
|
|
15
|
+
import { labelType } from '../rslang/labels';
|
|
16
|
+
import { TokenID } from '../rslang/parser/token';
|
|
17
|
+
import { TypeID } from '../rslang/semantic/typification';
|
|
18
|
+
|
|
19
|
+
import { type ArgumentValue, type Constituenta, type CstType, type RSForm } from './rsform';
|
|
20
|
+
import {
|
|
21
|
+
applyMappingToConstituents,
|
|
22
|
+
argumentValuesToMapping,
|
|
23
|
+
generateAlias,
|
|
24
|
+
inferTemplatedType,
|
|
25
|
+
isFunctional,
|
|
26
|
+
normalizeExpression
|
|
27
|
+
} from './rsform-api';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Payload for one {@link Constituenta} to create when instantiating an expression-bank template.
|
|
31
|
+
*
|
|
32
|
+
* Aliases are generated for the target schema. Formal definitions and typifications already
|
|
33
|
+
* reflect user argument substitution and remapping of bank aliases to those new aliases.
|
|
34
|
+
*/
|
|
35
|
+
export interface TemplateInstantiationItem {
|
|
36
|
+
/** Alias to assign in the target schema. */
|
|
37
|
+
alias: string;
|
|
38
|
+
/** Inferred type after argument substitution (function/predicate may become term/axiom). */
|
|
39
|
+
cst_type: CstType;
|
|
40
|
+
crucial: boolean;
|
|
41
|
+
convention: string;
|
|
42
|
+
definition_formal: string;
|
|
43
|
+
definition_raw: string;
|
|
44
|
+
term_raw: string;
|
|
45
|
+
term_forms: Constituenta['term_forms'];
|
|
46
|
+
typification_manual: string;
|
|
47
|
+
value_is_property: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Conflicting argument bindings inferred for the same template dependency. */
|
|
51
|
+
export interface TemplateBindingConflict {
|
|
52
|
+
templateAlias: string;
|
|
53
|
+
paramAlias: string;
|
|
54
|
+
existing: string;
|
|
55
|
+
incoming: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Batch create plan: dependencies first, selected template constituent last.
|
|
60
|
+
*
|
|
61
|
+
* Reuses constituents whose substituted formal definition already exists in the target schema
|
|
62
|
+
* (normalized comparison), even when the bank alias differs from the matched target alias.
|
|
63
|
+
* {@link TemplateInstantiationPlan.mainDuplicateAlias} reports when the selected template is a duplicate.
|
|
64
|
+
* Does not set `insert_after`; the caller supplies insertion on the batch DTO.
|
|
65
|
+
*/
|
|
66
|
+
export interface TemplateInstantiationPlan {
|
|
67
|
+
items: TemplateInstantiationItem[];
|
|
68
|
+
/** Set when the selected template expression already exists in the target schema. */
|
|
69
|
+
mainDuplicateAlias: string | null;
|
|
70
|
+
/** Set when nested template calls bind the same parameter to different global aliases. */
|
|
71
|
+
bindingConflict: TemplateBindingConflict | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Input for planning template instantiation into a conceptual schema.
|
|
76
|
+
*/
|
|
77
|
+
export interface TemplateInstantiationInput {
|
|
78
|
+
/** Schema that will receive new constituents. */
|
|
79
|
+
targetSchema: RSForm;
|
|
80
|
+
/** All constituents of the selected expression-bank RSForm (not only the prototype). */
|
|
81
|
+
templateItems: Constituenta[];
|
|
82
|
+
/** Template constituent chosen in the UI (main expression to add). */
|
|
83
|
+
prototype: Constituenta;
|
|
84
|
+
/** User bindings from template parameter aliases to target-schema global aliases. */
|
|
85
|
+
userArgs: ArgumentValue[];
|
|
86
|
+
/**
|
|
87
|
+
* Main constituent attributes from the create dialog (alias, labels, flags).
|
|
88
|
+
* `definition_formal` and `cst_type` may be overridden from `prototype` and `userArgs`.
|
|
89
|
+
*/
|
|
90
|
+
mainItem: TemplateInstantiationItem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface FuncCallRef {
|
|
94
|
+
callee: string;
|
|
95
|
+
args: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface TemplateParam {
|
|
99
|
+
alias: string;
|
|
100
|
+
typification: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a batch create plan for a template expression and its bank dependencies.
|
|
105
|
+
*
|
|
106
|
+
* Walks nested `F#` / `P#` calls in the substituted expression, collects missing term-functions
|
|
107
|
+
* and predicate-functions from `templateItems`, propagates argument bindings into nested calls,
|
|
108
|
+
* topologically orders dependencies, and remaps bank aliases to freshly generated target aliases.
|
|
109
|
+
*/
|
|
110
|
+
export function planTemplateInstantiation(input: TemplateInstantiationInput): TemplateInstantiationPlan {
|
|
111
|
+
return new TemplateInstantiationPlanner(input).plan();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stateful planner for {@link planTemplateInstantiation}.
|
|
116
|
+
*
|
|
117
|
+
* Prefer the function wrapper unless you need to hold or reuse planner state between calls.
|
|
118
|
+
*/
|
|
119
|
+
export class TemplateInstantiationPlanner {
|
|
120
|
+
private readonly targetSchema: RSForm;
|
|
121
|
+
private readonly prototype: Constituenta;
|
|
122
|
+
private readonly userArgs: ArgumentValue[];
|
|
123
|
+
private readonly mainItemTemplate: TemplateInstantiationItem;
|
|
124
|
+
private readonly templateByAlias: Map<string, Constituenta>;
|
|
125
|
+
private readonly userMapping: AliasMapping;
|
|
126
|
+
private readonly depBindings = new Map<string, Map<string, string>>();
|
|
127
|
+
private readonly depRefs = new Map<string, Set<string>>();
|
|
128
|
+
private bindingConflict: TemplateBindingConflict | null = null;
|
|
129
|
+
|
|
130
|
+
/** @param input - See {@link TemplateInstantiationInput}. */
|
|
131
|
+
constructor(input: TemplateInstantiationInput) {
|
|
132
|
+
this.targetSchema = input.targetSchema;
|
|
133
|
+
this.prototype = input.prototype;
|
|
134
|
+
this.userArgs = input.userArgs;
|
|
135
|
+
this.mainItemTemplate = input.mainItem;
|
|
136
|
+
this.templateByAlias = new Map(
|
|
137
|
+
input.templateItems.filter(cst => isFunctional(cst.cst_type)).map(cst => [cst.alias, cst])
|
|
138
|
+
);
|
|
139
|
+
this.userMapping = argumentValuesToMapping(input.userArgs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @returns Dependency items in topological order, then the main template item when it is not a duplicate. */
|
|
143
|
+
plan(): TemplateInstantiationPlan {
|
|
144
|
+
const mainDefinition = substituteTemplateArgs(this.prototype.definition_formal, this.userMapping);
|
|
145
|
+
this.collectDependencies(mainDefinition, this.userMapping);
|
|
146
|
+
|
|
147
|
+
if (this.bindingConflict) {
|
|
148
|
+
return { items: [], mainDuplicateAlias: null, bindingConflict: this.bindingConflict };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const definitionIndex = createFormalDefinitionIndex(this.targetSchema);
|
|
152
|
+
const dependencyItems = this.buildDependencyItems(definitionIndex);
|
|
153
|
+
const { item: mainItem, duplicateAlias: mainDuplicateAlias } = this.buildMainItem(
|
|
154
|
+
mainDefinition,
|
|
155
|
+
dependencyItems.takenAliases,
|
|
156
|
+
dependencyItems.aliasMapping,
|
|
157
|
+
definitionIndex
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
items: mainItem ? [...dependencyItems.items, mainItem] : dependencyItems.items,
|
|
162
|
+
mainDuplicateAlias,
|
|
163
|
+
bindingConflict: null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private buildDependencyItems(definitionIndex: FormalDefinitionIndex): {
|
|
168
|
+
items: TemplateInstantiationItem[];
|
|
169
|
+
aliasMapping: AliasMapping;
|
|
170
|
+
takenAliases: string[];
|
|
171
|
+
} {
|
|
172
|
+
const dependencyAliases = topologicalSort([...this.depBindings.keys()], this.depRefs);
|
|
173
|
+
const aliasMapping: AliasMapping = {};
|
|
174
|
+
const takenAliases: string[] = [];
|
|
175
|
+
const reservedAliases = [this.mainItemTemplate.alias];
|
|
176
|
+
const items: TemplateInstantiationItem[] = [];
|
|
177
|
+
|
|
178
|
+
for (const templateAlias of dependencyAliases) {
|
|
179
|
+
const template = this.templateByAlias.get(templateAlias)!;
|
|
180
|
+
const bindings = this.depBindings.get(templateAlias)!;
|
|
181
|
+
const args = bindingMapToArgs(getTemplateParams(template), bindings);
|
|
182
|
+
const cst_type = inferTemplatedType(template.cst_type, args);
|
|
183
|
+
const definition = applyAliasMapping(
|
|
184
|
+
substituteTemplateArgs(template.definition_formal, argumentValuesToMapping(args)),
|
|
185
|
+
aliasMapping
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const existingAlias = definitionIndex.find(cst_type, definition);
|
|
189
|
+
if (existingAlias) {
|
|
190
|
+
aliasMapping[templateAlias] = existingAlias;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const alias = generateAlias(template.cst_type, this.targetSchema, [...reservedAliases, ...takenAliases]);
|
|
195
|
+
takenAliases.push(alias);
|
|
196
|
+
aliasMapping[templateAlias] = alias;
|
|
197
|
+
|
|
198
|
+
const item = instantiateTemplateItem(template, args, alias);
|
|
199
|
+
item.definition_formal = definition;
|
|
200
|
+
applyMappingToConstituents([item], aliasMapping, false);
|
|
201
|
+
definitionIndex.register(cst_type, item.definition_formal, alias);
|
|
202
|
+
items.push(item);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { items, aliasMapping, takenAliases };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private buildMainItem(
|
|
209
|
+
mainDefinition: string,
|
|
210
|
+
takenAliases: string[],
|
|
211
|
+
aliasMapping: AliasMapping,
|
|
212
|
+
definitionIndex: FormalDefinitionIndex
|
|
213
|
+
): { item: TemplateInstantiationItem | null; duplicateAlias: string | null } {
|
|
214
|
+
const mappedDefinition = applyAliasMapping(mainDefinition, aliasMapping);
|
|
215
|
+
const cst_type = inferTemplatedType(this.prototype.cst_type, this.userArgs);
|
|
216
|
+
|
|
217
|
+
const duplicateAlias = definitionIndex.find(cst_type, mappedDefinition);
|
|
218
|
+
if (duplicateAlias) {
|
|
219
|
+
return { item: null, duplicateAlias };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const mainItem: TemplateInstantiationItem = {
|
|
223
|
+
...this.mainItemTemplate,
|
|
224
|
+
definition_formal: mappedDefinition,
|
|
225
|
+
cst_type
|
|
226
|
+
};
|
|
227
|
+
applyMappingToConstituents([mainItem], aliasMapping, false);
|
|
228
|
+
|
|
229
|
+
if (takenAliases.includes(mainItem.alias)) {
|
|
230
|
+
mainItem.alias = generateAlias(mainItem.cst_type, this.targetSchema, [
|
|
231
|
+
...takenAliases,
|
|
232
|
+
this.mainItemTemplate.alias
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { item: mainItem, duplicateAlias: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private ensureDep(templateAlias: string, bindings: Map<string, string>, referrer?: string): void {
|
|
240
|
+
if (this.bindingConflict) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!this.templateByAlias.has(templateAlias)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (referrer && this.depBindings.has(referrer)) {
|
|
247
|
+
const refs = this.depRefs.get(referrer) ?? new Set<string>();
|
|
248
|
+
refs.add(templateAlias);
|
|
249
|
+
this.depRefs.set(referrer, refs);
|
|
250
|
+
}
|
|
251
|
+
const mergeResult = mergeBindings(this.depBindings.get(templateAlias), bindings);
|
|
252
|
+
if (!mergeResult.ok) {
|
|
253
|
+
this.bindingConflict = { templateAlias, ...mergeResult.conflict };
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const merged = mergeResult.bindings;
|
|
257
|
+
const changed =
|
|
258
|
+
!this.depBindings.has(templateAlias) || merged.size !== (this.depBindings.get(templateAlias)?.size ?? 0);
|
|
259
|
+
this.depBindings.set(templateAlias, merged);
|
|
260
|
+
if (!changed && referrer) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const template = this.templateByAlias.get(templateAlias)!;
|
|
264
|
+
const args = bindingMapToArgs(getTemplateParams(template), merged);
|
|
265
|
+
const definition = substituteTemplateArgs(template.definition_formal, argumentValuesToMapping(args));
|
|
266
|
+
this.collectDependencies(definition, argumentValuesToMapping(args), templateAlias);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private collectDependencies(expression: string, scope: AliasMapping, referrer?: string): void {
|
|
270
|
+
if (this.bindingConflict) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const ast = parseRSLangExpression(expression);
|
|
274
|
+
if (!ast) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
for (const call of extractFuncCalls(ast, expression)) {
|
|
278
|
+
const template = this.templateByAlias.get(call.callee);
|
|
279
|
+
if (!template) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const scopedArgs = call.args.map(arg => substituteLocalAliases(arg, scope));
|
|
283
|
+
const bindings = bindingsFromCall(getTemplateParams(template), scopedArgs, scope);
|
|
284
|
+
this.ensureDep(call.callee, bindings, referrer);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ======= Internals ========
|
|
290
|
+
|
|
291
|
+
/** Normalized formal definitions in the target schema and in the current plan. */
|
|
292
|
+
class FormalDefinitionIndex {
|
|
293
|
+
private readonly byKey = new Map<string, string>();
|
|
294
|
+
|
|
295
|
+
register(cst_type: CstType, definition_formal: string, alias: string): void {
|
|
296
|
+
const normalized = normalizeExpression(definition_formal, cst_type);
|
|
297
|
+
if (!normalized) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.byKey.set(formalDefinitionKey(cst_type, normalized), alias);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
find(cst_type: CstType, definition_formal: string): string | null {
|
|
304
|
+
const normalized = normalizeExpression(definition_formal, cst_type);
|
|
305
|
+
if (!normalized) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return this.byKey.get(formalDefinitionKey(cst_type, normalized)) ?? null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function createFormalDefinitionIndex(schema: RSForm): FormalDefinitionIndex {
|
|
313
|
+
const index = new FormalDefinitionIndex();
|
|
314
|
+
for (const cst of schema.items) {
|
|
315
|
+
index.register(cst.cst_type, cst.definition_formal, cst.alias);
|
|
316
|
+
}
|
|
317
|
+
return index;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formalDefinitionKey(cst_type: CstType, normalized: string): string {
|
|
321
|
+
return `${cst_type}\0${normalized}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function instantiateTemplateItem(
|
|
325
|
+
template: Constituenta,
|
|
326
|
+
args: ArgumentValue[],
|
|
327
|
+
alias: string
|
|
328
|
+
): TemplateInstantiationItem {
|
|
329
|
+
return {
|
|
330
|
+
alias,
|
|
331
|
+
cst_type: inferTemplatedType(template.cst_type, args),
|
|
332
|
+
crucial: template.crucial,
|
|
333
|
+
convention: template.convention,
|
|
334
|
+
definition_formal: substituteTemplateArgs(template.definition_formal, argumentValuesToMapping(args)),
|
|
335
|
+
definition_raw: template.definition_raw,
|
|
336
|
+
term_raw: template.term_raw,
|
|
337
|
+
term_forms: [...template.term_forms],
|
|
338
|
+
typification_manual: template.typification_manual,
|
|
339
|
+
value_is_property: template.value_is_property
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function bindingsFromCall(params: TemplateParam[], callArgs: string[], scope: AliasMapping): Map<string, string> {
|
|
344
|
+
const bindings = new Map<string, string>();
|
|
345
|
+
params.forEach((param, index) => {
|
|
346
|
+
const value = resolveArgBinding(callArgs[index] ?? '', scope);
|
|
347
|
+
if (value) {
|
|
348
|
+
bindings.set(param.alias, value);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return bindings;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function resolveArgBinding(argExpr: string, scope: AliasMapping): string | undefined {
|
|
355
|
+
const scoped = substituteLocalAliases(argExpr, scope).trim();
|
|
356
|
+
if (isGlobalAlias(scoped)) {
|
|
357
|
+
return scoped;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
type MergeBindingsResult =
|
|
363
|
+
| { ok: true; bindings: Map<string, string> }
|
|
364
|
+
| { ok: false; conflict: { paramAlias: string; existing: string; incoming: string } };
|
|
365
|
+
|
|
366
|
+
function mergeBindings(existing: Map<string, string> | undefined, incoming: Map<string, string>): MergeBindingsResult {
|
|
367
|
+
const result = new Map(existing);
|
|
368
|
+
for (const [alias, value] of incoming) {
|
|
369
|
+
const existingValue = result.get(alias);
|
|
370
|
+
if (existingValue === undefined) {
|
|
371
|
+
result.set(alias, value);
|
|
372
|
+
} else if (existingValue !== value) {
|
|
373
|
+
return { ok: false, conflict: { paramAlias: alias, existing: existingValue, incoming: value } };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return { ok: true, bindings: result };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function bindingMapToArgs(params: TemplateParam[], bindings: Map<string, string>): ArgumentValue[] {
|
|
380
|
+
return params.map(param => ({
|
|
381
|
+
alias: param.alias,
|
|
382
|
+
typification: param.typification,
|
|
383
|
+
value: bindings.get(param.alias)
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getTemplateParams(template: Constituenta): TemplateParam[] {
|
|
388
|
+
const effectiveType = template.effectiveType;
|
|
389
|
+
if (effectiveType && (effectiveType.typeID === TypeID.function || effectiveType.typeID === TypeID.predicate)) {
|
|
390
|
+
return effectiveType.args.map(arg => ({
|
|
391
|
+
alias: arg.alias,
|
|
392
|
+
typification: labelType(arg.type)
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
const { head } = splitTemplateDefinition(template.definition_formal);
|
|
396
|
+
if (!head) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
return head.split(',').map(part => {
|
|
400
|
+
const trimmed = part.trim();
|
|
401
|
+
const separator = trimmed.indexOf('∈');
|
|
402
|
+
if (separator === -1) {
|
|
403
|
+
return { alias: trimmed, typification: '' };
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
alias: trimmed.slice(0, separator).trim(),
|
|
407
|
+
typification: trimmed.slice(separator + 1).trim()
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function extractFuncCalls(ast: AstNode, expression: string): FuncCallRef[] {
|
|
413
|
+
const result: FuncCallRef[] = [];
|
|
414
|
+
visitAstDFS(ast, node => {
|
|
415
|
+
if (node.typeID !== TokenID.NT_FUNC_CALL || node.children.length < 1) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const calleeNode = node.children[0];
|
|
419
|
+
if (
|
|
420
|
+
calleeNode.typeID !== TokenID.ID_FUNCTION &&
|
|
421
|
+
calleeNode.typeID !== TokenID.ID_PREDICATE &&
|
|
422
|
+
calleeNode.typeID !== TokenID.ID_GLOBAL
|
|
423
|
+
) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const callee = expression.slice(calleeNode.from, calleeNode.to);
|
|
427
|
+
if (!/^[FP]\d+$/.test(callee)) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
result.push({
|
|
431
|
+
callee,
|
|
432
|
+
args: node.children.slice(1).map(child => expression.slice(child.from, child.to).trim())
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function topologicalSort(aliases: string[], refs: Map<string, Set<string>>): string[] {
|
|
439
|
+
const remaining = new Set(aliases);
|
|
440
|
+
const sorted: string[] = [];
|
|
441
|
+
while (remaining.size > 0) {
|
|
442
|
+
const ready = [...remaining].filter(alias => {
|
|
443
|
+
const deps = refs.get(alias) ?? new Set<string>();
|
|
444
|
+
return [...deps].every(dep => !remaining.has(dep));
|
|
445
|
+
});
|
|
446
|
+
if (ready.length === 0) {
|
|
447
|
+
return [...remaining, ...sorted].reverse();
|
|
448
|
+
}
|
|
449
|
+
ready.sort();
|
|
450
|
+
for (const alias of ready) {
|
|
451
|
+
sorted.push(alias);
|
|
452
|
+
remaining.delete(alias);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return sorted;
|
|
456
|
+
}
|
package/src/rslang/api.test.ts
CHANGED
|
@@ -3,11 +3,28 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import {
|
|
4
4
|
applyTypificationMapping,
|
|
5
5
|
extractGlobals,
|
|
6
|
+
isGlobalAlias,
|
|
6
7
|
isSetTypification,
|
|
7
8
|
isSimpleExpression,
|
|
9
|
+
parseRSLangExpression,
|
|
8
10
|
splitTemplateDefinition
|
|
9
11
|
} from './api';
|
|
10
12
|
|
|
13
|
+
describe('Testing isGlobalAlias', () => {
|
|
14
|
+
it.each([
|
|
15
|
+
['X1', true],
|
|
16
|
+
['F42', true],
|
|
17
|
+
['P6', true],
|
|
18
|
+
['S1', true],
|
|
19
|
+
['X1×X2', false],
|
|
20
|
+
['F42[α]', false],
|
|
21
|
+
['α', false],
|
|
22
|
+
['', false]
|
|
23
|
+
])('isGlobalAlias("%s") -> %s', (value, expected) => {
|
|
24
|
+
expect(isGlobalAlias(value)).toBe(expected);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
11
28
|
const globalsData: [string, string][] = [
|
|
12
29
|
['', ''],
|
|
13
30
|
['X1', 'X1'],
|
|
@@ -114,3 +131,14 @@ describe('Testing isSetTypification', () => {
|
|
|
114
131
|
});
|
|
115
132
|
});
|
|
116
133
|
});
|
|
134
|
+
|
|
135
|
+
describe('parseRSLangExpression', () => {
|
|
136
|
+
it('returns null for blank input', () => {
|
|
137
|
+
expect(parseRSLangExpression('')).toBeNull();
|
|
138
|
+
expect(parseRSLangExpression(' ')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('parses non-blank expressions', () => {
|
|
142
|
+
expect(parseRSLangExpression('1 = 2')?.hasError).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/rslang/api.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module: API for RSLanguage.
|
|
3
3
|
*/
|
|
4
|
-
import { type AstNode } from '../parsing';
|
|
4
|
+
import { type AstNode, buildTree } from '../parsing';
|
|
5
5
|
|
|
6
|
+
import { normalizeAST } from './parser/normalize';
|
|
7
|
+
import { parser as rslangParser } from './parser/parser';
|
|
6
8
|
import { ArgumentsExtractor, type ArgumentsType } from './semantic/arguments-extractor';
|
|
7
9
|
export { generateExpressionFromAst } from './parser/expression-generator';
|
|
8
10
|
|
|
@@ -11,11 +13,34 @@ export type AliasMapping = Record<string, string>;
|
|
|
11
13
|
|
|
12
14
|
// cspell:disable
|
|
13
15
|
const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
|
|
14
|
-
const
|
|
16
|
+
const GLOBAL_ALIAS_SOURCE = '[XCSADFPTN]\\d+';
|
|
17
|
+
const GLOBALS_REGEXP = new RegExp(GLOBAL_ALIAS_SOURCE, 'g');
|
|
18
|
+
const GLOBAL_ALIAS_ANCHORED_REGEXP = new RegExp(`^${GLOBAL_ALIAS_SOURCE}$`);
|
|
15
19
|
const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
|
|
16
20
|
const TYPIFICATION_SET = /^ℬ+\([ℬ\(((X|C)\d+|Z)\)×]*\)$/g;
|
|
17
21
|
// cspell:enable
|
|
18
22
|
|
|
23
|
+
/** Whether {@link value} is a single schema global alias (e.g. `X1`, `F42`). */
|
|
24
|
+
export function isGlobalAlias(value: string): boolean {
|
|
25
|
+
return GLOBAL_ALIAS_ANCHORED_REGEXP.test(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a RSLang expression into a normalized AST.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` for blank input. Parses the original `expression` string so node
|
|
32
|
+
* offsets match the source text (required for slice-based AST walks).
|
|
33
|
+
*/
|
|
34
|
+
export function parseRSLangExpression(expression: string): AstNode | null {
|
|
35
|
+
if (!expression.trim()) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const tree = rslangParser.parse(expression);
|
|
39
|
+
const ast = buildTree(tree.cursor());
|
|
40
|
+
normalizeAST(ast, expression);
|
|
41
|
+
return ast;
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
/** Extract arguments from AST. */
|
|
20
45
|
export function extractArguments(ast: AstNode): ArgumentsType[] {
|
|
21
46
|
return new ArgumentsExtractor().run(ast);
|
|
@@ -100,6 +125,11 @@ export function substituteTemplateArgs(expression: string, mapping: AliasMapping
|
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
/** Substitute local parameter aliases in an expression fragment. */
|
|
129
|
+
export function substituteLocalAliases(expression: string, mapping: AliasMapping): string {
|
|
130
|
+
return applyPattern(expression, mapping, LOCALS_REGEXP);
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
/** Apply alias mapping. */
|
|
104
134
|
export function applyAliasMapping(target: string, mapping: AliasMapping): string {
|
|
105
135
|
return applyPattern(target, mapping, GLOBALS_REGEXP);
|
package/src/rslang/labels.ts
CHANGED
|
@@ -138,6 +138,15 @@ export function labelRSLangNode(node: AstNodeBase): string {
|
|
|
138
138
|
return 'UNKNOWN NODE: ' + String(node.typeID);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/** Formats a base-set alias for display (mangled radicals `func<R#>` → `<func>R#`). */
|
|
142
|
+
export function labelBaseId(baseID: string): string {
|
|
143
|
+
const match = /^([^<]+)<(R[1-9]\d*)>$/.exec(baseID);
|
|
144
|
+
if (match) {
|
|
145
|
+
return `<${match[1]}>${match[2]}`;
|
|
146
|
+
}
|
|
147
|
+
return baseID;
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
/** Converts expression type to string. */
|
|
142
151
|
export function labelType(type: ExpressionType | null): string {
|
|
143
152
|
if (!type) {
|
|
@@ -149,7 +158,7 @@ export function labelType(type: ExpressionType | null): string {
|
|
|
149
158
|
case TypeID.integer:
|
|
150
159
|
return INTEGER_TYPE_NAME;
|
|
151
160
|
case TypeID.basic:
|
|
152
|
-
return type.baseID;
|
|
161
|
+
return labelBaseId(type.baseID);
|
|
153
162
|
case TypeID.tuple:
|
|
154
163
|
return type.factors
|
|
155
164
|
.map(factor => (factor.typeID === TypeID.tuple ? `(${labelType(factor)})` : labelType(factor)))
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { type AstNode
|
|
1
|
+
import { type AstNode } from '../../parsing';
|
|
2
|
+
import { parseRSLangExpression } from '../api';
|
|
2
3
|
import { annotateError } from '../ast-annotations';
|
|
3
4
|
import { RSErrorCode, type RSErrorDescription } from '../error';
|
|
4
5
|
import { labelType } from '../labels';
|
|
5
|
-
import { normalizeAST } from '../parser/normalize';
|
|
6
|
-
import { parser as rslangParser } from '../parser/parser';
|
|
7
6
|
import { extractSyntaxErrors } from '../parser/syntax-errors';
|
|
8
7
|
import { TokenID } from '../parser/token';
|
|
9
8
|
|
|
@@ -74,10 +73,10 @@ export class RSLangAnalyzer {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
public checkFast(expression: string, options?: AnalysisOptions): AnalysisFast {
|
|
77
|
-
|
|
76
|
+
const ast = parseRSLangExpression(expression);
|
|
77
|
+
if (!ast) {
|
|
78
78
|
return { success: false, type: null, valueClass: null, ast: null };
|
|
79
79
|
}
|
|
80
|
-
const ast = this.parse(expression);
|
|
81
80
|
if (ast.hasError) {
|
|
82
81
|
return { success: false, type: null, valueClass: null, ast: ast };
|
|
83
82
|
}
|
|
@@ -109,11 +108,11 @@ export class RSLangAnalyzer {
|
|
|
109
108
|
const reporter = (error: RSErrorDescription) => {
|
|
110
109
|
errors.push(error);
|
|
111
110
|
};
|
|
112
|
-
|
|
111
|
+
const ast = parseRSLangExpression(expression);
|
|
112
|
+
if (!ast) {
|
|
113
113
|
reporter({ code: RSErrorCode.cstEmptyDerived, from: 0, to: 0 });
|
|
114
114
|
return { success: false, type: null, valueClass: null, errors: errors, ast: null };
|
|
115
115
|
}
|
|
116
|
-
const ast = this.parse(expression);
|
|
117
116
|
if (ast.hasError) {
|
|
118
117
|
extractSyntaxErrors(ast, expression, reporter, options?.annotateErrors ?? false, {
|
|
119
118
|
expected: options?.expected
|
|
@@ -154,13 +153,6 @@ export class RSLangAnalyzer {
|
|
|
154
153
|
: this.valueAuditor.run(ast, reporter, options?.annotateErrors ?? false);
|
|
155
154
|
return { success: true, type, valueClass, errors, ast };
|
|
156
155
|
}
|
|
157
|
-
|
|
158
|
-
private parse(expression: string): AstNode {
|
|
159
|
-
const tree = rslangParser.parse(expression);
|
|
160
|
-
const ast = buildTree(tree.cursor());
|
|
161
|
-
normalizeAST(ast, expression);
|
|
162
|
-
return ast;
|
|
163
|
-
}
|
|
164
156
|
}
|
|
165
157
|
|
|
166
158
|
// ======= Internals ========
|
|
@@ -159,12 +159,12 @@ const errorData = [
|
|
|
159
159
|
// Functions
|
|
160
160
|
['F1[X1]', { code: RSErrorCode.invalidArgsArity, from: 0, to: 6, params: ['2', '1'] }],
|
|
161
161
|
['F1[X1, X1, X1]', { code: RSErrorCode.invalidArgsArity, from: 0, to: 14, params: ['2', '3'] }],
|
|
162
|
-
['F1[X1, S1]', { code: RSErrorCode.invalidArgumentType, from: 7, to: 9, params: ['b
|
|
163
|
-
['F1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['a
|
|
162
|
+
['F1[X1, S1]', { code: RSErrorCode.invalidArgumentType, from: 7, to: 9, params: ['F1<b>∈ℬ(X1)', 'ℬ(X1×X1)'] }],
|
|
163
|
+
['F1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['F1<a>∈ℬ(X1)', 'ℬ(X1×X1)'] }],
|
|
164
164
|
['P1[X1]', { code: RSErrorCode.invalidArgsArity, from: 0, to: 6, params: ['2', '1'] }],
|
|
165
165
|
['P1[X1, X1, X1]', { code: RSErrorCode.invalidArgsArity, from: 0, to: 14, params: ['2', '3'] }],
|
|
166
|
-
['P1[X1, S1]', { code: RSErrorCode.invalidArgumentType, from: 7, to: 9, params: ['b
|
|
167
|
-
['P1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['a
|
|
166
|
+
['P1[X1, S1]', { code: RSErrorCode.invalidArgumentType, from: 7, to: 9, params: ['P1<b>∈ℬ(X1)', 'ℬ(X1×X1)'] }],
|
|
167
|
+
['P1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['P1<a>∈ℬ(X1)', 'ℬ(X1×X1)'] }],
|
|
168
168
|
// Integral
|
|
169
169
|
['card(debool(X1))', { code: RSErrorCode.invalidCard, from: 5, to: 15, params: ['X1'] }],
|
|
170
170
|
['card(S3)', { code: RSErrorCode.invalidCard, from: 5, to: 7, params: ['C1'] }],
|
|
@@ -313,7 +313,26 @@ describe('TypeAuditor', () => {
|
|
|
313
313
|
code: RSErrorCode.invalidArgumentType,
|
|
314
314
|
from: 7,
|
|
315
315
|
to: 12,
|
|
316
|
-
params: ['b
|
|
316
|
+
params: ['F2<b>∈X1', 'ℬℬ(X1)']
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('Qualifies unresolved radicals and parameter aliases in invalidArgumentType message', () => {
|
|
321
|
+
typeContext.set('P44', {
|
|
322
|
+
typeID: TypeID.predicate,
|
|
323
|
+
result: LogicT,
|
|
324
|
+
args: [
|
|
325
|
+
{ alias: 'α', type: bool(tuple([tuple([basic('R1'), basic('R2')]), basic('C1')])) },
|
|
326
|
+
{ alias: 'β', type: bool(tuple([tuple([basic('R2'), basic('R3')]), basic('C1')])) }
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
const inner = tuple([basic('X1'), basic('X2')]);
|
|
330
|
+
typeContext.set('S9', bool(tuple([inner, basic('C1')])));
|
|
331
|
+
expectError('P44[S9, S9]', {
|
|
332
|
+
code: RSErrorCode.invalidArgumentType,
|
|
333
|
+
from: 8,
|
|
334
|
+
to: 10,
|
|
335
|
+
params: ['P44<β>∈ℬ((X2×<P44>R3)×C1)', 'ℬ((X1×X2)×C1)']
|
|
317
336
|
});
|
|
318
337
|
});
|
|
319
338
|
|
|
@@ -331,7 +350,7 @@ describe('TypeAuditor', () => {
|
|
|
331
350
|
code: RSErrorCode.invalidArgumentType,
|
|
332
351
|
from: 7,
|
|
333
352
|
to: 9,
|
|
334
|
-
params: ['
|
|
353
|
+
params: ['P2<ζ>∈ℬℬ(X1)', 'Z']
|
|
335
354
|
});
|
|
336
355
|
});
|
|
337
356
|
|