Skip to content

Commit

Permalink
check variable and global reference in issue*
Browse files Browse the repository at this point in the history
  • Loading branch information
joswig committed Feb 5, 2025
1 parent 72d1616 commit ff2c562
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 44 deletions.
9 changes: 6 additions & 3 deletions src/components/sequencing/SequenceEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
IOutputFormat,
ISequenceAdaptation,
LibrarySequence,
LibrarySequenceMap,
Parcel,
TimeTagInfo,
} from '../../types/sequencing';
Expand Down Expand Up @@ -114,7 +115,7 @@
let commandDictionary: CommandDictionary | null;
let disableCopyAndExport: boolean = true;
let parameterDictionaries: ParameterDictionary[] = [];
let librarySequenceMap: { [sequenceName: string]: LibrarySequence } = {};
let librarySequenceMap: LibrarySequenceMap = {};
let librarySequences: LibrarySequence[] = [];
let commandFormBuilderGrid: string;
let editorOutputDiv: HTMLDivElement;
Expand Down Expand Up @@ -225,7 +226,9 @@
),
});
editorSequenceView.dispatch({
effects: compartmentSeqLinter.reconfigure(vmlLinter(commandDictionary, librarySequenceMap)),
effects: compartmentSeqLinter.reconfigure(
vmlLinter(commandDictionary, librarySequenceMap, $sequenceAdaptation.globals ?? []),
),
});
editorSequenceView.dispatch({
effects: compartmentSeqTooltip.reconfigure(vmlTooltip(commandDictionary, librarySequenceMap)),
Expand Down Expand Up @@ -512,7 +515,7 @@
function getCommandDef(
commandDictionary: CommandDictionary | null,
librarySequenceMap: { [sequenceName: string]: LibrarySequence },
librarySequenceMap: LibrarySequenceMap,
stemName: string,
): FswCommand | null {
const commandDefFromCommandDictionary = commandDictionary?.fswCommandMap[stemName];
Expand Down
2 changes: 2 additions & 0 deletions src/types/sequencing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export function isLibrarySequence(obj: unknown): obj is LibrarySequence {
return !!obj && (obj as LibrarySequence).type === 'librarySequence';
}

export type LibrarySequenceMap = { [sequenceName: string]: LibrarySequence };

export type UserSequenceInsertInput = Omit<UserSequence, 'created_at' | 'id' | 'owner' | 'updated_at'>;

export type Workspace = {
Expand Down
71 changes: 60 additions & 11 deletions src/utilities/codemirror/vml/vmlLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import type { SyntaxNode, Tree } from '@lezer/common';
import type { CommandDictionary, FswCommand, FswCommandArgument } from '@nasa-jpl/aerie-ampcs';
import type { EditorView } from 'codemirror';
import { closest } from 'fastest-levenshtein';
import type { LibrarySequence } from '../../../types/sequencing';
import { filterNodes } from '../../sequence-editor/tree-utils';
import type { GlobalType } from '../../../types/global-type';
import type { LibrarySequenceMap } from '../../../types/sequencing';
import { filterNodes, getNearestAncestorNodeOfType } from '../../sequence-editor/tree-utils';
import { quoteEscape, unquoteUnescape } from '../codemirror-utils';
import { VmlLanguage } from './vml';
import {
Expand All @@ -15,14 +16,18 @@ import {
RULE_FUNCTION_NAME,
RULE_ISSUE,
RULE_ISSUE_DYNAMIC,
RULE_PARAMETER,
RULE_SPAWN,
RULE_TIME_TAGGED_STATEMENTS,
RULE_VARIABLE_NAME,
TOKEN_DOUBLE_CONST,
TOKEN_ERROR,
TOKEN_HEX_CONST,
TOKEN_INT_CONST,
TOKEN_STRING_CONST,
TOKEN_UINT_CONST,
} from './vmlConstants';
import { getVmlVariables } from './vmlTreeUtils';

/**
* Limitations
Expand All @@ -38,7 +43,8 @@ const MAX_PARSER_ERRORS = 100;

export function vmlLinter(
commandDictionary: CommandDictionary | null = null,
librarySequenceMap: { [sequenceName: string]: LibrarySequence } = {},
librarySequenceMap: LibrarySequenceMap = {},
globals: GlobalType[],
): Extension {
return linter(view => {
const diagnostics: Diagnostic[] = [];
Expand All @@ -50,15 +56,52 @@ export function vmlLinter(
}

const parsed = VmlLanguage.parser.parse(sequence);
diagnostics.push(...validateCommands(commandDictionary, librarySequenceMap, sequence, parsed));
diagnostics.push(...validateCommands(commandDictionary, librarySequenceMap, globals, sequence, parsed));
diagnostics.push(...validateGlobals(sequence, tree, globals));

return diagnostics;
});
}

function validateGlobals(input: string, tree: Tree, globals: GlobalType[]): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
const globalNames: Set<string> = new Set(globals.map(g => g.name));

// for each block, sequence, etc -- determine what variables are declared
const declaredVariables: { [to: number]: Set<string> } = {};
for (const node of filterNodes(tree.cursor(), node => node.name === RULE_TIME_TAGGED_STATEMENTS)) {
declaredVariables[node.from] = new Set(getVmlVariables(input, tree, node.to));
}

// check all variables
for (const node of filterNodes(tree.cursor(), node => node.name === RULE_VARIABLE_NAME)) {
// don't check variable declarations
if (!getNearestAncestorNodeOfType(node, [RULE_PARAMETER])) {
const variableReference = input.slice(node.from, node.to);
if (!globalNames.has(variableReference)) {
const timeTaggedStatementsNode = getNearestAncestorNodeOfType(node, [RULE_TIME_TAGGED_STATEMENTS]);
if (timeTaggedStatementsNode) {
const variablesInScope = declaredVariables[timeTaggedStatementsNode.from];
if (!variablesInScope.has(variableReference)) {
const alternative = closest(variableReference, Array.from(globalNames.union(variablesInScope)));
diagnostics.push(suggestAlternative(node, variableReference, 'symbolic reference', alternative));
}
}
}
}

if (diagnostics.length >= 10) {
// stop checking to avoid flood of errors if adaptation is misconfigured
break;
}
}
return diagnostics;
}

function validateCommands(
commandDictionary: CommandDictionary,
librarySequenceMap: { [sequenceName: string]: LibrarySequence },
librarySequenceMap: LibrarySequenceMap,
globals: GlobalType[],
docText: string,
parsed: Tree,
): Diagnostic[] {
Expand All @@ -68,9 +111,9 @@ function validateCommands(
const { node } = cursor;
const tokenType = node.type.name;
if (tokenType === RULE_ISSUE || tokenType === RULE_ISSUE_DYNAMIC) {
diagnostics.push(...validateIssue(node, docText, commandDictionary, tokenType));
diagnostics.push(...validateIssue(node, docText, commandDictionary, globals, tokenType));
} else if (tokenType === RULE_SPAWN) {
diagnostics.push(...validateSpawn(node, docText, librarySequenceMap));
diagnostics.push(...validateSpawn(node, docText, librarySequenceMap, globals));
}
} while (cursor.next());
return diagnostics;
Expand All @@ -80,6 +123,7 @@ function validateIssue(
node: SyntaxNode,
docText: string,
commandDictionary: CommandDictionary,
globals: GlobalType[],
tokenType: string,
): Diagnostic[] {
const isDynamic = tokenType === RULE_ISSUE_DYNAMIC;
Expand All @@ -94,7 +138,7 @@ function validateIssue(
const alternativeStem = isDynamic ? quoteEscape(closestStem) : closestStem;
return [suggestAlternative(stemNameNode, stemName, 'command', alternativeStem)];
} else {
return validateArguments(commandDictionary, commandDef, node, stemNameNode, docText, isDynamic ? 1 : 0);
return validateArguments(commandDictionary, globals, commandDef, node, stemNameNode, docText, isDynamic ? 1 : 0);
}
}
return [];
Expand All @@ -103,7 +147,8 @@ function validateIssue(
function validateSpawn(
node: SyntaxNode,
docText: string,
librarySequenceMap: { [sequenceName: string]: LibrarySequence },
librarySequenceMap: LibrarySequenceMap,
globals: GlobalType[],
): Diagnostic[] {
const spawnedNameNode = node.getChild(RULE_FUNCTION_NAME);
if (spawnedNameNode) {
Expand All @@ -119,7 +164,9 @@ function validateSpawn(
),
];
} else {
// Check arguments
if (globals) {
// Check arguments
}
}
}
return [];
Expand Down Expand Up @@ -151,6 +198,7 @@ function suggestAlternative(node: SyntaxNode, current: string, typeLabel: string

function validateArguments(
commandDictionary: CommandDictionary,
globals: GlobalType[],
commandDef: FswCommand,
functionNode: SyntaxNode,
functionNameNode: SyntaxNode,
Expand All @@ -167,7 +215,7 @@ function validateArguments(

if (argDef && argNode) {
// validate expected argument
diagnostics.push(...validateArgument(commandDictionary, argDef, argNode, docText));
diagnostics.push(...validateArgument(commandDictionary, globals, argDef, argNode, docText));
} else if (!argNode && !!argDef) {
const { from, to } = functionNameNode;
diagnostics.push({
Expand Down Expand Up @@ -195,6 +243,7 @@ function validateArguments(

function validateArgument(
commandDictionary: CommandDictionary,
globals: GlobalType[],
argDef: FswCommandArgument,
argNode: SyntaxNode,
docText: string,
Expand Down
64 changes: 34 additions & 30 deletions src/utilities/codemirror/vml/vmlTreeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,36 +112,7 @@ export class VmlCommandInfoMapper implements CommandInfoMapper {
}

getVariables(docText: string, tree: Tree, position: number): string[] {
// VML Variable_declaration_with_optional_tlm_id are per module (only 1 module per file)
// VML Common_Function may contain Parameters and Variable_declarations

const moduleVariables = filterNodesToArray(
tree.cursor(),
node => node.name === RULE_VARIABLE_DECLARATION_WITH_OPTIONAL_TLM_ID,
)
.map(node =>
node
.getChild(RULE_VARIABLE_DECLARATION_TYPE)
?.getChild(RULE_VARIABLE_NAME_CONSTANT)
?.getChild(RULE_VARIABLE_NAME),
)
.filter(filterEmpty)
.map(node => docText.slice(node.from, node.to));

const positionNode = tree.resolveInner(position);
const commonFunctionNode = getNearestAncestorNodeOfType(positionNode, [RULE_COMMON_FUNCTION]);
if (commonFunctionNode) {
const subTreeOffset = commonFunctionNode.from;
const commonFunctionParametersAndVariables = filterNodesToArray(commonFunctionNode.toTree().cursor(), node =>
[RULE_INPUT_PARAMETER, RULE_INPUT_OUTPUT_PARAMETER, RULE_VARIABLE_NAME_CONSTANT].includes(node.name),
)
.map(node => node.getChild(RULE_VARIABLE_NAME))
.filter(filterEmpty)
.map(node => docText.slice(subTreeOffset + node.from, subTreeOffset + node.to));
return [...moduleVariables, ...commonFunctionParametersAndVariables];
}

return moduleVariables;
return getVmlVariables(docText, tree, position);
}

isArgumentNodeOfVariableType(argNode: SyntaxNode | null): boolean {
Expand Down Expand Up @@ -169,6 +140,39 @@ export class VmlCommandInfoMapper implements CommandInfoMapper {
}
}

export function getVmlVariables(docText: string, tree: Tree, position: number): string[] {
// VML Variable_declaration_with_optional_tlm_id are per module (only 1 module per file)
// VML Common_Function may contain Parameters and Variable_declarations

const moduleVariables = filterNodesToArray(
tree.cursor(),
node => node.name === RULE_VARIABLE_DECLARATION_WITH_OPTIONAL_TLM_ID,
)
.map(node =>
node
.getChild(RULE_VARIABLE_DECLARATION_TYPE)
?.getChild(RULE_VARIABLE_NAME_CONSTANT)
?.getChild(RULE_VARIABLE_NAME),
)
.filter(filterEmpty)
.map(node => docText.slice(node.from, node.to));

const positionNode = tree.resolveInner(position);
const commonFunctionNode = getNearestAncestorNodeOfType(positionNode, [RULE_COMMON_FUNCTION]);
if (commonFunctionNode) {
const subTreeOffset = commonFunctionNode.from;
const commonFunctionParametersAndVariables = filterNodesToArray(commonFunctionNode.toTree().cursor(), node =>
[RULE_INPUT_PARAMETER, RULE_INPUT_OUTPUT_PARAMETER, RULE_VARIABLE_NAME_CONSTANT].includes(node.name),
)
.map(node => node.getChild(RULE_VARIABLE_NAME))
.filter(filterEmpty)
.map(node => docText.slice(subTreeOffset + node.from, subTreeOffset + node.to));
return [...moduleVariables, ...commonFunctionParametersAndVariables];
}

return moduleVariables;
}

export function getVmlNameNode(timeTaggedStatementNode: SyntaxNode | null): SyntaxNode | null {
const ruleStatementNode = timeTaggedStatementNode?.getChild(RULE_STATEMENT);

Expand Down

0 comments on commit ff2c562

Please sign in to comment.