@rsconcept/domain 1.8.0 → 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.
Files changed (50) hide show
  1. package/dist/{analyzer-DpE0qknt.d.ts → analyzer-iqAXyROE.d.ts} +2 -2
  2. package/dist/{arguments-extractor-Dd1lFom9.d.ts → arguments-extractor-ngatANBI.d.ts} +2 -2
  3. package/dist/{ast-annotations-CZCzs9Uk.d.ts → ast-annotations-D6KZDu12.d.ts} +2 -2
  4. package/dist/{calculator-Dt03dsnd.d.ts → calculator-k8BGIKmj.d.ts} +2 -2
  5. package/dist/index.d.ts +8 -8
  6. package/dist/library/index.d.ts +13 -3
  7. package/dist/library/index.js +36 -8
  8. package/dist/library/index.js.map +1 -1
  9. package/dist/library/rsengine.d.ts +1 -1
  10. package/dist/library/rsform-api.d.ts +2 -2
  11. package/dist/library/rsform.d.ts +2 -2
  12. package/dist/library/rsmodel-api.d.ts +1 -1
  13. package/dist/library/structure-planner.d.ts +1 -1
  14. package/dist/rslang/api.d.ts +1 -1
  15. package/dist/rslang/api.js +2 -1
  16. package/dist/rslang/api.js.map +1 -1
  17. package/dist/rslang/ast-annotations.d.ts +1 -1
  18. package/dist/rslang/eval/calculator.d.ts +1 -1
  19. package/dist/rslang/eval/value-api.d.ts +1 -1
  20. package/dist/rslang/index.d.ts +8 -8
  21. package/dist/rslang/labels.d.ts +4 -2
  22. package/dist/rslang/labels.js +8 -2
  23. package/dist/rslang/labels.js.map +1 -1
  24. package/dist/rslang/parser/syntax-errors/index.d.ts +1 -1
  25. package/dist/rslang/semantic/analyzer.d.ts +1 -1
  26. package/dist/rslang/semantic/arguments-extractor.d.ts +1 -1
  27. package/dist/rslang/semantic/type-auditor.d.ts +1 -1
  28. package/dist/rslang/semantic/type-auditor.js +2 -2
  29. package/dist/rslang/semantic/type-auditor.js.map +1 -1
  30. package/dist/rslang/semantic/typification-api.d.ts +1 -1
  31. package/dist/rslang/semantic/typification-parser.d.ts +1 -1
  32. package/dist/rslang/semantic/typification.d.ts +2 -2
  33. package/dist/rslang/semantic/typification.js +14 -6
  34. package/dist/rslang/semantic/typification.js.map +1 -1
  35. package/dist/rslang/typification-graph.d.ts +1 -1
  36. package/dist/{typification-DDuv88ir.d.ts → typification-9lNu9R4k.d.ts} +8 -4
  37. package/dist/{typification-graph-8VdK6_Fh.d.ts → typification-graph-BY_oGT82.d.ts} +2 -2
  38. package/dist/{typification-parser-DHO42ZRc.d.ts → typification-parser-Dytsv9CI.d.ts} +2 -2
  39. package/dist/{value-api-C2X4Pom5.d.ts → value-api-CXLl_zg0.d.ts} +2 -2
  40. package/package.json +1 -1
  41. package/src/library/index.ts +1 -0
  42. package/src/library/template-instantiation.test.ts +122 -3
  43. package/src/library/template-instantiation.ts +42 -12
  44. package/src/rslang/api.ts +2 -1
  45. package/src/rslang/labels.ts +10 -1
  46. package/src/rslang/semantic/type-auditor.test.ts +25 -6
  47. package/src/rslang/semantic/type-auditor.ts +2 -1
  48. package/src/rslang/semantic/typification-api.test.ts +21 -5
  49. package/src/rslang/semantic/typification-parser.test.ts +6 -1
  50. package/src/rslang/semantic/typification.ts +16 -6
@@ -219,17 +219,82 @@ describe('planTemplateInstantiation', () => {
219
219
  expect(plan.items[0].definition_formal).not.toContain('F42[');
220
220
  });
221
221
 
222
- it('skips template constituents that already exist in the target schema', () => {
222
+ it('instantiates bank dependency when target alias collides but definition differs', () => {
223
+ const schemaWithUnrelatedF42 = mockSchema([
224
+ ...targetSchema.items,
225
+ mockCst({
226
+ id: 12,
227
+ alias: 'F42',
228
+ cst_type: CstType.FUNCTION,
229
+ definition_formal: '[α∈R1] α'
230
+ })
231
+ ]);
232
+
233
+ const plan = planTemplateInstantiation({
234
+ targetSchema: schemaWithUnrelatedF42,
235
+ templateItems,
236
+ prototype: p30,
237
+ userArgs: [{ alias: 'σ', typification: 'ℬ((R1×R1)×R1)', value: 'S2' }],
238
+ mainItem: {
239
+ alias: 'A2',
240
+ cst_type: CstType.PREDICATE,
241
+ crucial: false,
242
+ convention: '',
243
+ definition_formal: p30.definition_formal,
244
+ definition_raw: '',
245
+ term_raw: p30.term_raw,
246
+ term_forms: [],
247
+ typification_manual: '',
248
+ value_is_property: false
249
+ }
250
+ });
251
+
252
+ const dependency = plan.items[0];
253
+ expect(plan.items).toHaveLength(2);
254
+ expect(dependency.alias).not.toBe('F42');
255
+ expect(dependency.definition_formal).toContain('S2');
256
+ expect(plan.items.at(-1)!.definition_formal).toContain(`${dependency.alias}[`);
257
+ expect(plan.items.at(-1)!.definition_formal).not.toContain('F42[');
258
+ });
259
+
260
+ it('reuses template dependency when target has matching substituted definition', () => {
261
+ const userArgs: ArgumentValue[] = [{ alias: 'σ', typification: 'ℬ((R1×R1)×R1)', value: 'S2' }];
262
+
263
+ const baseline = planTemplateInstantiation({
264
+ targetSchema,
265
+ templateItems,
266
+ prototype: p30,
267
+ userArgs,
268
+ mainItem: {
269
+ alias: 'A2',
270
+ cst_type: CstType.PREDICATE,
271
+ crucial: false,
272
+ convention: '',
273
+ definition_formal: p30.definition_formal,
274
+ definition_raw: '',
275
+ term_raw: p30.term_raw,
276
+ term_forms: [],
277
+ typification_manual: '',
278
+ value_is_property: false
279
+ }
280
+ });
281
+
282
+ const equivalentFunction = baseline.items[0];
223
283
  const schemaWithF42 = mockSchema([
224
284
  ...targetSchema.items,
225
- mockCst({ id: 12, alias: 'F42', cst_type: CstType.FUNCTION, definition_formal: f42.definition_formal })
285
+ mockCst({
286
+ id: 12,
287
+ alias: 'F42',
288
+ cst_type: equivalentFunction.cst_type,
289
+ definition_formal: equivalentFunction.definition_formal
290
+ })
226
291
  ]);
227
292
 
228
293
  const plan = planTemplateInstantiation({
229
294
  targetSchema: schemaWithF42,
230
295
  templateItems,
231
296
  prototype: p30,
232
- userArgs: [{ alias: 'σ', typification: 'ℬ((R1×R1)×R1)', value: 'S2' }],
297
+ userArgs,
233
298
  mainItem: {
234
299
  alias: 'A2',
235
300
  cst_type: CstType.PREDICATE,
@@ -305,4 +370,58 @@ describe('planTemplateInstantiation', () => {
305
370
  expect(plan.mainDuplicateAlias).toBe('A9');
306
371
  expect(plan.items.some(item => item.term_raw === p35.term_raw)).toBe(false);
307
372
  });
373
+
374
+ it('reports binding conflict when the same template parameter is bound to different aliases', () => {
375
+ const f10 = mockCst({
376
+ id: 5,
377
+ alias: 'F10',
378
+ cst_type: CstType.FUNCTION,
379
+ term_raw: 'g',
380
+ definition_formal: '[x∈R1] x'
381
+ });
382
+ const p11 = mockCst({
383
+ id: 6,
384
+ alias: 'P11',
385
+ cst_type: CstType.PREDICATE,
386
+ term_raw: 'h',
387
+ definition_formal: '[y∈R1] F10[y]'
388
+ });
389
+ const mainTemplate = mockCst({
390
+ id: 7,
391
+ alias: 'P12',
392
+ cst_type: CstType.PREDICATE,
393
+ term_raw: 'main',
394
+ definition_formal: '[a∈R1, b∈R1] P11[a] & P11[b]'
395
+ });
396
+
397
+ const plan = planTemplateInstantiation({
398
+ targetSchema,
399
+ templateItems: [f10, p11],
400
+ prototype: mainTemplate,
401
+ userArgs: [
402
+ { alias: 'a', typification: 'R1', value: 'S1' },
403
+ { alias: 'b', typification: 'R1', value: 'S2' }
404
+ ],
405
+ mainItem: {
406
+ alias: 'A1',
407
+ cst_type: CstType.PREDICATE,
408
+ crucial: false,
409
+ convention: '',
410
+ definition_formal: mainTemplate.definition_formal,
411
+ definition_raw: '',
412
+ term_raw: mainTemplate.term_raw,
413
+ term_forms: [],
414
+ typification_manual: '',
415
+ value_is_property: false
416
+ }
417
+ });
418
+
419
+ expect(plan.bindingConflict).toEqual({
420
+ templateAlias: 'P11',
421
+ paramAlias: 'y',
422
+ existing: 'S1',
423
+ incoming: 'S2'
424
+ });
425
+ expect(plan.items).toHaveLength(0);
426
+ });
308
427
  });
@@ -47,11 +47,19 @@ export interface TemplateInstantiationItem {
47
47
  value_is_property: boolean;
48
48
  }
49
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
+
50
58
  /**
51
59
  * Batch create plan: dependencies first, selected template constituent last.
52
60
  *
53
- * Skips bank functions/predicates whose alias already exists in the target schema, and
54
- * constituents whose substituted formal definition already exists (normalized comparison).
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.
55
63
  * {@link TemplateInstantiationPlan.mainDuplicateAlias} reports when the selected template is a duplicate.
56
64
  * Does not set `insert_after`; the caller supplies insertion on the batch DTO.
57
65
  */
@@ -59,6 +67,8 @@ export interface TemplateInstantiationPlan {
59
67
  items: TemplateInstantiationItem[];
60
68
  /** Set when the selected template expression already exists in the target schema. */
61
69
  mainDuplicateAlias: string | null;
70
+ /** Set when nested template calls bind the same parameter to different global aliases. */
71
+ bindingConflict: TemplateBindingConflict | null;
62
72
  }
63
73
 
64
74
  /**
@@ -115,6 +125,7 @@ export class TemplateInstantiationPlanner {
115
125
  private readonly userMapping: AliasMapping;
116
126
  private readonly depBindings = new Map<string, Map<string, string>>();
117
127
  private readonly depRefs = new Map<string, Set<string>>();
128
+ private bindingConflict: TemplateBindingConflict | null = null;
118
129
 
119
130
  /** @param input - See {@link TemplateInstantiationInput}. */
120
131
  constructor(input: TemplateInstantiationInput) {
@@ -133,6 +144,10 @@ export class TemplateInstantiationPlanner {
133
144
  const mainDefinition = substituteTemplateArgs(this.prototype.definition_formal, this.userMapping);
134
145
  this.collectDependencies(mainDefinition, this.userMapping);
135
146
 
147
+ if (this.bindingConflict) {
148
+ return { items: [], mainDuplicateAlias: null, bindingConflict: this.bindingConflict };
149
+ }
150
+
136
151
  const definitionIndex = createFormalDefinitionIndex(this.targetSchema);
137
152
  const dependencyItems = this.buildDependencyItems(definitionIndex);
138
153
  const { item: mainItem, duplicateAlias: mainDuplicateAlias } = this.buildMainItem(
@@ -144,7 +159,8 @@ export class TemplateInstantiationPlanner {
144
159
 
145
160
  return {
146
161
  items: mainItem ? [...dependencyItems.items, mainItem] : dependencyItems.items,
147
- mainDuplicateAlias
162
+ mainDuplicateAlias,
163
+ bindingConflict: null
148
164
  };
149
165
  }
150
166
 
@@ -221,7 +237,10 @@ export class TemplateInstantiationPlanner {
221
237
  }
222
238
 
223
239
  private ensureDep(templateAlias: string, bindings: Map<string, string>, referrer?: string): void {
224
- if (targetHasAlias(this.targetSchema, templateAlias) || !this.templateByAlias.has(templateAlias)) {
240
+ if (this.bindingConflict) {
241
+ return;
242
+ }
243
+ if (!this.templateByAlias.has(templateAlias)) {
225
244
  return;
226
245
  }
227
246
  if (referrer && this.depBindings.has(referrer)) {
@@ -229,7 +248,12 @@ export class TemplateInstantiationPlanner {
229
248
  refs.add(templateAlias);
230
249
  this.depRefs.set(referrer, refs);
231
250
  }
232
- const merged = mergeBindings(this.depBindings.get(templateAlias), bindings);
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;
233
257
  const changed =
234
258
  !this.depBindings.has(templateAlias) || merged.size !== (this.depBindings.get(templateAlias)?.size ?? 0);
235
259
  this.depBindings.set(templateAlias, merged);
@@ -243,6 +267,9 @@ export class TemplateInstantiationPlanner {
243
267
  }
244
268
 
245
269
  private collectDependencies(expression: string, scope: AliasMapping, referrer?: string): void {
270
+ if (this.bindingConflict) {
271
+ return;
272
+ }
246
273
  const ast = parseRSLangExpression(expression);
247
274
  if (!ast) {
248
275
  return;
@@ -332,14 +359,21 @@ function resolveArgBinding(argExpr: string, scope: AliasMapping): string | undef
332
359
  return undefined;
333
360
  }
334
361
 
335
- function mergeBindings(existing: Map<string, string> | undefined, incoming: Map<string, string>): Map<string, string> {
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 {
336
367
  const result = new Map(existing);
337
368
  for (const [alias, value] of incoming) {
338
- if (!result.has(alias)) {
369
+ const existingValue = result.get(alias);
370
+ if (existingValue === undefined) {
339
371
  result.set(alias, value);
372
+ } else if (existingValue !== value) {
373
+ return { ok: false, conflict: { paramAlias: alias, existing: existingValue, incoming: value } };
340
374
  }
341
375
  }
342
- return result;
376
+ return { ok: true, bindings: result };
343
377
  }
344
378
 
345
379
  function bindingMapToArgs(params: TemplateParam[], bindings: Map<string, string>): ArgumentValue[] {
@@ -401,10 +435,6 @@ function extractFuncCalls(ast: AstNode, expression: string): FuncCallRef[] {
401
435
  return result;
402
436
  }
403
437
 
404
- function targetHasAlias(schema: RSForm, alias: string): boolean {
405
- return schema.items.some(cst => cst.alias === alias);
406
- }
407
-
408
438
  function topologicalSort(aliases: string[], refs: Map<string, Set<string>>): string[] {
409
439
  const remaining = new Set(aliases);
410
440
  const sorted: string[] = [];
package/src/rslang/api.ts CHANGED
@@ -15,13 +15,14 @@ export type AliasMapping = Record<string, string>;
15
15
  const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
16
16
  const GLOBAL_ALIAS_SOURCE = '[XCSADFPTN]\\d+';
17
17
  const GLOBALS_REGEXP = new RegExp(GLOBAL_ALIAS_SOURCE, 'g');
18
+ const GLOBAL_ALIAS_ANCHORED_REGEXP = new RegExp(`^${GLOBAL_ALIAS_SOURCE}$`);
18
19
  const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
19
20
  const TYPIFICATION_SET = /^ℬ+\([ℬ\(((X|C)\d+|Z)\)×]*\)$/g;
20
21
  // cspell:enable
21
22
 
22
23
  /** Whether {@link value} is a single schema global alias (e.g. `X1`, `F42`). */
23
24
  export function isGlobalAlias(value: string): boolean {
24
- return new RegExp(`^${GLOBAL_ALIAS_SOURCE}$`).test(value);
25
+ return GLOBAL_ALIAS_ANCHORED_REGEXP.test(value);
25
26
  }
26
27
 
27
28
  /**
@@ -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)))
@@ -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∈ℬ(X1)', 'ℬ(X1×X1)'] }],
163
- ['F1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['a∈ℬ(X1)', 'ℬ(X1×X1)'] }],
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∈ℬ(X1)', 'ℬ(X1×X1)'] }],
167
- ['P1[S1, X1]', { code: RSErrorCode.invalidArgumentType, from: 3, to: 5, params: ['a∈ℬ(X1)', 'ℬ(X1×X1)'] }],
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∈X1', 'ℬℬ(X1)']
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: ['ζ∈ℬℬ(X1)', 'Z']
353
+ params: ['P2<ζ>∈ℬℬ(X1)', 'Z']
335
354
  });
336
355
  });
337
356
 
@@ -20,6 +20,7 @@ import {
20
20
  isTemplateRadical,
21
21
  isTypification,
22
22
  LogicT,
23
+ mangleParamAlias,
23
24
  mangleRadicalId,
24
25
  type Parametrized,
25
26
  tuple,
@@ -1025,7 +1026,7 @@ export class TypeAuditor {
1025
1026
  substituteBase(expectedType, substitutes);
1026
1027
  }
1027
1028
  this.onError(RSErrorCode.invalidArgumentType, node.children[child], [
1028
- `${type.args[child - 1].alias}∈${labelType(expectedType)}`,
1029
+ `${mangleParamAlias(type.args[child - 1].alias, alias)}∈${labelType(expectedType)}`,
1029
1030
  labelType(childType)
1030
1031
  ]);
1031
1032
  return null;
@@ -1,7 +1,16 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import type { TypePath } from './typification';
4
- import { AnyTypificationT, basic, bool, IntegerT, mangleRadicalId, tuple, type Typification } from './typification';
4
+ import {
5
+ AnyTypificationT,
6
+ basic,
7
+ bool,
8
+ IntegerT,
9
+ mangleParamAlias,
10
+ mangleRadicalId,
11
+ tuple,
12
+ type Typification
13
+ } from './typification';
5
14
  import { applyPath, cloneTypification, compareTemplated, substituteBase } from './typification-api';
6
15
 
7
16
  describe('applyPath', () => {
@@ -56,15 +65,22 @@ describe('compareTemplated', () => {
56
65
 
57
66
  describe('mangleRadicalId', () => {
58
67
  it('qualifies template radical with function name', () => {
59
- expect(mangleRadicalId('R1', 'P2')).toBe('R1<P2>');
60
- expect(mangleRadicalId('R2', 'F10')).toBe('R2<F10>');
68
+ expect(mangleRadicalId('R1', 'P2')).toBe('P2<R1>');
69
+ expect(mangleRadicalId('R2', 'F10')).toBe('F10<R2>');
70
+ });
71
+ });
72
+
73
+ describe('mangleParamAlias', () => {
74
+ it('qualifies formal parameter alias with calling function name', () => {
75
+ expect(mangleParamAlias('β', 'P44')).toBe('P44<β>');
76
+ expect(mangleParamAlias('a', 'F1')).toBe('F1<a>');
61
77
  });
62
78
  });
63
79
 
64
80
  describe('cloneTypification + substituteBase', () => {
65
81
  it('resolves mangled radicals for error messages', () => {
66
- const argType = bool(basic('R1<F2>'));
67
- const substitutes = new Map<string, Typification>([['R1<F2>', basic('X1')]]);
82
+ const argType = bool(basic('F2<R1>'));
83
+ const substitutes = new Map<string, Typification>([['F2<R1>', basic('X1')]]);
68
84
  const expectedType = cloneTypification(argType);
69
85
  substituteBase(expectedType, substitutes);
70
86
  expect(expectedType).toEqual(bool(basic('X1')));
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { labelType } from '../labels';
3
+ import { labelBaseId, labelType } from '../labels';
4
4
 
5
5
  import { TypeID } from './typification';
6
6
  import { applyAsciiTypeSubstitutions, parseTypeText } from './typification-parser';
@@ -47,4 +47,9 @@ describe('parseTypeText', () => {
47
47
  expect(applyAsciiTypeSubstitutions('X1*X2 -> X3')).toBe('X1×X2 → X3');
48
48
  expect(applyAsciiTypeSubstitutions('B(X1)')).toBe('ℬ(X1)');
49
49
  });
50
+
51
+ it('displays mangled radicals with function qualifier before radical', () => {
52
+ expect(labelBaseId('P44<R3>')).toBe('<P44>R3');
53
+ expect(labelType({ typeID: TypeID.basic, baseID: 'P2<R1>' })).toBe('<P2>R1');
54
+ });
50
55
  });
@@ -174,17 +174,27 @@ export function isTypification(type: ExpressionType | null): boolean {
174
174
  );
175
175
  }
176
176
 
177
- /** Checks if given alias is a template or mangled radical. */
178
- export function isRadical(alias: string): boolean {
179
- return /^R[1-9]\d*(?:<[^>]+>)?$/.test(alias);
180
- }
181
-
182
177
  /** Checks if given alias is an unmangled template radical. */
183
178
  export function isTemplateRadical(alias: string): boolean {
184
179
  return /^R[1-9]\d*$/.test(alias);
185
180
  }
186
181
 
182
+ /** Checks if given alias is a mangled template radical (`funcName<R#>`). */
183
+ export function isMangledRadical(alias: string): boolean {
184
+ return /^[^<]+<R[1-9]\d*>$/.test(alias);
185
+ }
186
+
187
+ /** Checks if given alias is a template or mangled radical. */
188
+ export function isRadical(alias: string): boolean {
189
+ return isTemplateRadical(alias) || isMangledRadical(alias);
190
+ }
191
+
187
192
  /** Mangle template radical with calling function name. */
188
193
  export function mangleRadicalId(radical: string, funcName: string): string {
189
- return `${radical}<${funcName}>`;
194
+ return `${funcName}<${radical}>`;
195
+ }
196
+
197
+ /** Mangle formal parameter alias with calling function name (for diagnostics). */
198
+ export function mangleParamAlias(alias: string, funcName: string): string {
199
+ return `${funcName}<${alias}>`;
190
200
  }