ConceptPortal-public/rsconcept/frontend/src/features/rsform/models/rslang-api.ts
Ivan 29af778d54
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions
R: Remove unused dependencies
2025-04-13 23:14:00 +03:00

293 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Module: API for RSLanguage.
*/
import { type Tree } from '@lezer/common';
import { cursorNode } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
import { CstType, type IRSErrorDescription, type RSErrorType } from '../backend/types';
import { type AliasMapping, type IArgumentValue, RSErrorClass, type SyntaxTree } from './rslang';
// cspell:disable
const LOCALS_REGEXP = /[_a-zα-ω][a-zα-ω]*\d*/g;
const GLOBALS_REGEXP = /[XCSADFPT]\d+/g;
const COMPLEX_SYMBOLS_REGEXP = /[∀∃×ℬ;|:]/g;
const TYPIFICATION_SET = /^+\([\(X\d+\)×]*\)$/g;
// cspell:enable
/**
* Extracts global variable names from a given expression.
*/
export function extractGlobals(expression: string): Set<string> {
return new Set(expression.match(GLOBALS_REGEXP) ?? []);
}
/**
* Check if expression is simple derivation.
*/
export function isSimpleExpression(text: string): boolean {
return !text.match(COMPLEX_SYMBOLS_REGEXP);
}
/**
* Check if expression is set typification.
*/
export function isSetTypification(text: string): boolean {
return !!text.match(TYPIFICATION_SET);
}
/**
* Infers type of constituent for a given template and arguments.
*/
export function inferTemplatedType(templateType: CstType, args: IArgumentValue[]): CstType {
if (args.length === 0 || args.some(arg => !arg.value)) {
return templateType;
} else if (templateType === CstType.PREDICATE) {
return CstType.AXIOM;
} else {
return CstType.TERM;
}
}
/**
* Splits a string containing a template definition into its head and body parts.
*
* A template definition is expected to have the following format: `[head] body`.
* If the input string does not contain the opening square bracket '[', the entire
* string is treated as the body, and an empty string is assigned to the head.
* If the opening bracket is present, the function attempts to find the matching
* closing bracket ']' to determine the head and body parts.
*
* @example
* const template = '[header] body content';
* const result = splitTemplateDefinition(template);
* // result: `{ head: 'header', body: 'body content' }`
*/
export function splitTemplateDefinition(target: string) {
let start = 0;
for (; start < target.length && target[start] !== '['; ++start);
if (start < target.length) {
for (let counter = 0, end = start + 1; end < target.length; ++end) {
if (target[end] === '[') {
++counter;
} else if (target[end] === ']') {
if (counter !== 0) {
--counter;
} else {
return {
head: target.substring(start + 1, end).trim(),
body: target.substring(end + 1).trim()
};
}
}
}
}
return {
head: '',
body: target
};
}
/**
* Substitutes values for template arguments in a given expression.
*
* This function takes an input mathematical expression and a list of argument values.
* It replaces template argument placeholders in the expression with their corresponding values
* from the provided arguments.
*/
export function substituteTemplateArgs(expression: string, args: IArgumentValue[]): string {
if (args.every(arg => !arg.value)) {
return expression;
}
const mapping: AliasMapping = {};
args
.filter(arg => !!arg.value)
.forEach(arg => {
mapping[arg.alias] = arg.value!;
});
let { head, body } = splitTemplateDefinition(expression);
body = applyPattern(body, mapping, LOCALS_REGEXP);
const argTexts = head.split(',').map(text => text.trim());
head = argTexts
.filter(arg => [...arg.matchAll(LOCALS_REGEXP)].every(local => local.every(match => !(match in mapping))))
.join(', ');
if (!head) {
return body;
} else {
return `[${head}] ${body}`;
}
}
/**
* Generate ErrorID label.
*/
export function getRSErrorPrefix(error: IRSErrorDescription): string {
const id = error.errorType.toString(16);
// prettier-ignore
switch(inferErrorClass(error.errorType)) {
case RSErrorClass.LEXER: return 'L' + id;
case RSErrorClass.PARSER: return 'P' + id;
case RSErrorClass.SEMANTIC: return 'S' + id;
case RSErrorClass.UNKNOWN: return 'U' + id;
}
}
/**
* Apply alias mapping.
*/
export function applyAliasMapping(target: string, mapping: AliasMapping): string {
return applyPattern(target, mapping, GLOBALS_REGEXP);
}
/**
* Apply alias typification mapping.
*/
export function applyTypificationMapping(target: string, mapping: AliasMapping): string {
const modified = applyAliasMapping(target, mapping);
if (modified === target) {
return target;
}
const deleteBrackets: number[] = [];
const positions: number[] = [];
const booleans: number[] = [];
let boolCount: number = 0;
let stackSize: number = 0;
for (let i = 0; i < modified.length; i++) {
const char = modified[i];
if (char === '') {
boolCount++;
continue;
}
if (char === '(') {
stackSize++;
positions.push(i);
booleans.push(boolCount);
}
boolCount = 0;
if (char === ')') {
if (
i < modified.length - 1 &&
modified[i + 1] === ')' &&
stackSize > 1 &&
positions[stackSize - 2] + booleans[stackSize - 1] + 1 === positions[stackSize - 1]
) {
deleteBrackets.push(i);
deleteBrackets.push(positions[stackSize - 2]);
}
if (i === modified.length - 1 && stackSize === 1 && positions[0] === 0) {
deleteBrackets.push(i);
deleteBrackets.push(positions[0]);
}
stackSize--;
positions.pop();
booleans.pop();
}
}
let result = '';
for (let i = 0; i < modified.length; i++) {
if (!deleteBrackets.includes(i)) {
result += modified[i];
}
}
return result;
}
/**
* Transform Tree to {@link SyntaxTree}.
*/
export function transformAST(tree: Tree): SyntaxTree {
const result: SyntaxTree = [];
const parents: number[] = [];
const cursor = tree.cursor();
let finished = false;
let leave = true;
while (!finished) {
let node = cursorNode(cursor);
node.isLeaf = !cursor.firstChild();
leave = true;
result.push({
uid: result.length,
parent: parents.length > 0 ? parents[parents.length - 1] : result.length,
typeID: node.type.id,
start: node.from,
finish: node.to,
data: {
dataType: 'string',
value: node.type.name == '⚠' ? PARAMETER.errorNodeLabel : node.type.name
}
});
parents.push(result.length - 1);
if (!node.isLeaf) continue;
for (;;) {
node = cursorNode(cursor, node.isLeaf);
if (leave) {
parents.pop();
}
leave = cursor.type.isAnonymous;
node.isLeaf = false;
if (cursor.nextSibling()) {
break;
}
if (!cursor.parent()) {
finished = true;
break;
}
leave = true;
}
}
return result;
}
// ====== Internals =========
/** Text substitution guided by mapping and regular expression. */
function applyPattern(text: string, mapping: AliasMapping, pattern: RegExp): string {
if (text === '' || pattern === null) {
return text;
}
let posInput = 0;
let output = '';
const patternMatches = text.matchAll(pattern);
for (const segment of patternMatches) {
const entity = segment[0];
const start = segment.index ?? 0;
if (entity in mapping) {
output += text.substring(posInput, start);
output += mapping[entity];
posInput = start + segment[0].length;
}
}
output += text.substring(posInput);
return output;
}
const ERROR_LEXER_MASK = 512;
const ERROR_PARSER_MASK = 1024;
const ERROR_SEMANTIC_MASK = 2048;
/** Infers error class from error type (code). */
function inferErrorClass(error: RSErrorType): RSErrorClass {
if ((error & ERROR_LEXER_MASK) !== 0) {
return RSErrorClass.LEXER;
} else if ((error & ERROR_PARSER_MASK) !== 0) {
return RSErrorClass.PARSER;
} else if ((error & ERROR_SEMANTIC_MASK) !== 0) {
return RSErrorClass.SEMANTIC;
} else {
return RSErrorClass.UNKNOWN;
}
}