From 6f02bf7e852e9b642b775feb87352f8124e6720a Mon Sep 17 00:00:00 2001 From: Gideon Koenig Date: Mon, 20 Jan 2025 20:52:47 +0100 Subject: [PATCH 1/4] feat: rework the service registration, rework the exposed methods --- .../src/language/communication/rpc.ts | 40 ++- .../graphical-editor/ast-parser/argument.ts | 29 ++ .../graphical-editor/ast-parser/call.ts | 236 ++++++++++++++++ .../graphical-editor/ast-parser/edge.ts | 55 ++++ .../graphical-editor/ast-parser/expression.ts | 53 ++++ .../graphical-editor/ast-parser/parameter.ts | 24 ++ .../graphical-editor/ast-parser/parser.ts | 150 +++++++++++ .../ast-parser/placeholder.ts | 19 ++ .../graphical-editor/ast-parser/result.ts | 18 ++ .../graphical-editor/ast-parser/segment.ts | 55 ++++ .../graphical-editor/ast-parser/statement.ts | 106 ++++++++ .../ast-parser/tools/debug-utils.ts | 69 +++++ .../graphical-editor/ast-parser/utils.ts | 18 ++ .../src/language/graphical-editor/global.ts | 54 ++++ .../graphical-editor-provider.ts | 253 ++++++++++++++++++ .../src/language/safe-ds-module.ts | 7 + 16 files changed, 1185 insertions(+), 1 deletion(-) create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parameter.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/placeholder.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/result.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/tools/debug-utils.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/global.ts create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts diff --git a/packages/safe-ds-lang/src/language/communication/rpc.ts b/packages/safe-ds-lang/src/language/communication/rpc.ts index d05dd3239..5ce216938 100644 --- a/packages/safe-ds-lang/src/language/communication/rpc.ts +++ b/packages/safe-ds-lang/src/language/communication/rpc.ts @@ -1,6 +1,8 @@ import { MessageDirection, NotificationType0, RequestType0 } from 'vscode-languageserver'; -import { NotificationType } from 'vscode-languageserver-protocol'; +import { NotificationType, RequestType } from 'vscode-languageserver-protocol'; import { UUID } from 'node:crypto'; +import { Buildin, Collection } from '../graphical-editor/global.js'; +import { Uri } from 'vscode'; export namespace InstallRunnerNotification { export const method = 'runner/install' as const; @@ -91,3 +93,39 @@ export namespace IsRunnerReadyRequest { export const messageDirection = MessageDirection.clientToServer; export const type = new RequestType0(method); } + +export namespace GraphicalEditorSyncEventNotification { + export const method = 'graphical-editor/sync-event' as const; + export const messageDirection = MessageDirection.serverToClient; + export const type = new NotificationType(method); +} + +export namespace GraphicalEditorOpenSyncChannelRequest { + export const method = 'graphical-editor/openSyncChannel' as const; + export const messageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} + +export namespace GraphicalEditorCloseSyncChannelRequest { + export const method = 'graphical-editor/closeSyncChannel' as const; + export const messageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} + +export namespace GraphicalEditorGetDocumentationRequest { + export const method = 'graphical-editor/getDocumentation' as const; + export const messageDirection = MessageDirection.clientToServer; + export const type = new RequestType<{ uri: Uri; uniquePath: string }, string | undefined, void>(method); +} + +export namespace GraphicalEditorGetBuildinsRequest { + export const method = 'graphical-editor/getBuildins' as const; + export const messageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} + +export namespace GraphicalEditorParseDocumentRequest { + export const method = 'graphical-editor/parseDocument' as const; + export const messageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts new file mode 100644 index 000000000..0f7e226c8 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts @@ -0,0 +1,29 @@ +import { isSdsLiteral, SdsArgument } from '../../generated/ast.js'; +import { CustomError } from '../global.js'; +import { Call } from './call.js'; +import { Placeholder } from './placeholder.js'; +import { Expression, GenericExpression } from './expression.js'; +import { Parameter } from './parameter.js'; +import { Parser } from './parser.js'; + +export class Argument { + constructor( + public readonly text: string, + public readonly reference: GenericExpression | Call | Placeholder | Parameter | undefined, + public readonly parameterName?: string, + ) {} + + public static parse(node: SdsArgument, parser: Parser) { + if (!node.value.$cstNode) return parser.pushError('CstNode missing', node.value); + const text = node.value.$cstNode.text; + + let expression; + if (!isSdsLiteral(node.value)) expression = Expression.parse(node.value, parser); + if (expression instanceof CustomError) return expression; + + if (node.parameter && !node.parameter.ref) return parser.pushError('Missing Parameterreference', node); + const parameterName = node.parameter?.ref?.name; + + return new Argument(text, expression, parameterName); + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts new file mode 100644 index 000000000..c50c202d2 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts @@ -0,0 +1,236 @@ +import { + SdsCall, + SdsClass, + SdsExpression, + SdsFunction, + SdsMemberAccess, + SdsPlaceholder, + SdsReference, + SdsSegment, + isSdsCall, + isSdsClass, + isSdsFunction, + isSdsMemberAccess, + isSdsPlaceholder, + isSdsReference, + isSdsSegment, +} from '../../generated/ast.js'; +import { CustomError } from '../global.js'; +import { Argument } from './argument.js'; +import { Edge, Port } from './edge.js'; +import { GenericExpression } from './expression.js'; +import { Parameter } from './parameter.js'; +import { Placeholder } from './placeholder.js'; +import { Result } from './result.js'; +import { filterErrors } from './utils.js'; +import { Parser } from './parser.js'; + +export class Call { + private constructor( + public readonly id: number, + public readonly name: string, + public readonly self: string | undefined, + public readonly parameterList: Parameter[], + public readonly resultList: Result[], + public readonly category: string, + public readonly uniquePath: string, + ) {} + + public static parse(node: SdsCall, parser: Parser): Call | CustomError { + const id = parser.getNewId(); + + if (!isValidCallReceiver(node.receiver)) { + return parser.pushError(`Invalid Call receiver: ${debugInvalidCallReceiver(node.receiver)}`, node.receiver); + } + + let name = ''; + let self: string | undefined = undefined; + let category = ''; + let argumentList: Argument[] = []; + let parameterList: Parameter[] = []; + let resultList: Result[] = []; + + argumentList = filterErrors(node.argumentList.arguments.map((argument) => Argument.parse(argument, parser))); + + if (isSdsMemberAccess(node.receiver)) { + const tmp = Call.parseSelf(node.receiver, id, parser); + if (tmp instanceof CustomError) return tmp; + self = tmp; + + const functionDeclaration = node.receiver.member.target.ref; + name = functionDeclaration.name; + category = parser.getCategory(functionDeclaration)?.name ?? ''; + + resultList = filterErrors( + (functionDeclaration.resultList?.results ?? []).map((result) => Result.parse(result, parser)), + ); + parameterList = filterErrors( + (functionDeclaration.parameterList?.parameters ?? []).map((parameter) => + Parameter.parse(parameter, parser), + ), + ); + } + + if (isSdsReference(node.receiver) && isSdsClass(node.receiver.target.ref)) { + const classDeclaration = node.receiver.target.ref; + + name = 'new'; + self = classDeclaration.name; + category = 'Modeling'; + + if (!classDeclaration.parameterList) + return parser.pushError('Missing constructor parameters', classDeclaration); + parameterList = filterErrors( + classDeclaration.parameterList.parameters.map((parameter) => Parameter.parse(parameter, parser)), + ); + resultList = [new Result('new', classDeclaration.name)]; + } + + if (isSdsReference(node.receiver) && isSdsSegment(node.receiver.target.ref)) { + const segmentDeclaration = node.receiver.target.ref; + + self = ''; + name = segmentDeclaration.name; + category = 'Segment'; + + resultList = filterErrors( + (segmentDeclaration.resultList?.results ?? []).map((result) => Result.parse(result, parser)), + ); + parameterList = filterErrors( + (segmentDeclaration.parameterList?.parameters ?? []).map((parameter) => + Parameter.parse(parameter, parser), + ), + ); + } + + const parameterListCompleted = matchArgumentsToParameter(parameterList, argumentList, node, id, parser); + if (parameterListCompleted instanceof CustomError) return parameterListCompleted; + + const call = new Call(id, name, self, parameterListCompleted, resultList, category, parser.getUniquePath(node)); + parser.graph.callList.push(call); + return call; + } + + private static parseSelf(node: CallReceiver, id: number, parser: Parser) { + if (isSdsMemberAccess(node)) { + if (isSdsCall(node.receiver)) { + const call = Call.parse(node.receiver, parser); + if (call instanceof CustomError) return call; + + if (call.resultList.length > 1) return parser.pushError('To many result', node.receiver); + if (call.resultList.length < 1) return parser.pushError('Missing result', node.receiver); + + Edge.create(Port.fromResult(call.resultList[0]!, call.id), Port.fromName(id, 'self'), parser); + } else if (isSdsReference(node.receiver)) { + const receiver = node.receiver.target.ref; + + if (isSdsClass(receiver)) { + return receiver.name; + } else if (isSdsPlaceholder(receiver)) { + const placeholder = Placeholder.parse(receiver, parser); + Edge.create(Port.fromPlaceholder(placeholder, false), Port.fromName(id, 'self'), parser); + } + } + } + return ''; + } +} + +const matchArgumentsToParameter = ( + parameterList: Parameter[], + argumentList: Argument[], + callNode: SdsCall, + id: number, + parser: Parser, +): Parameter[] | CustomError => { + for (const [i, parameter] of parameterList.entries()) { + const argumentIndexMatched = argumentList[i]; + if (argumentIndexMatched instanceof CustomError) return argumentIndexMatched; + + const argumentNameMatched = argumentList.find( + (argument) => !(argument instanceof CustomError) && argument.parameterName === parameter.name, + ) as Argument | undefined; + + if (argumentIndexMatched && argumentNameMatched && argumentIndexMatched !== argumentNameMatched) + return parser.pushError(`To many matches for ${parameter.name}`, callNode.argumentList); + const argument = argumentIndexMatched ?? argumentNameMatched; + + if (argument) { + parameter.argumentText = argument.text; + if (argument.reference instanceof Call) { + const call = argument.reference; + if (call.resultList.length !== 1) return parser.pushError('Type missmatch', callNode.argumentList); + Edge.create(Port.fromResult(call.resultList[0]!, call.id), Port.fromParameter(parameter, id), parser); + } + if (argument.reference instanceof GenericExpression) { + const experession = argument.reference; + Edge.create(Port.fromGenericExpression(experession, false), Port.fromParameter(parameter, id), parser); + } + if (argument.reference instanceof Placeholder) { + const placeholder = argument.reference; + Edge.create(Port.fromPlaceholder(placeholder, false), Port.fromParameter(parameter, id), parser); + } + if (argument.reference instanceof Parameter) { + const segmentParameter = argument.reference; + Edge.create(Port.fromParameter(segmentParameter, -1), Port.fromParameter(parameter, id), parser); + } + continue; + } + + if (!argument && parameter.defaultValue) { + continue; + } + + if (!argument && !parameter.defaultValue) { + return parser.pushError(`Missing Argument for ${parameter.name}`, callNode); + } + } + return parameterList; +}; + +type CallReceiver = + | (SdsReference & { target: { ref: SdsClass | SdsSegment } }) + | (SdsMemberAccess & { + member: { + target: { ref: SdsFunction }; + }; + receiver: SdsCall | { target: { ref: SdsPlaceholder | SdsClass } }; + }); + +const isValidCallReceiver = (receiver: SdsExpression): receiver is CallReceiver => { + /* eslint-disable no-implicit-coercion */ + return ( + (isSdsMemberAccess(receiver) && + !!receiver.member && + !!receiver.member.target.ref && + isSdsFunction(receiver.member.target.ref) && + ((isSdsReference(receiver.receiver) && + (isSdsClass(receiver.receiver.target.ref) || isSdsPlaceholder(receiver.receiver.target.ref))) || + isSdsCall(receiver.receiver))) || + (isSdsReference(receiver) && (isSdsClass(receiver.target.ref) || isSdsSegment(receiver.target.ref))) + ); +}; + +const debugInvalidCallReceiver = (receiver: SdsExpression): string => { + /* eslint-disable no-implicit-coercion */ + if (isSdsMemberAccess(receiver)) { + if (!receiver.member) return 'MemberAccess: Missing member'; + if (!receiver.member.target.ref) return 'MemberAccess: Missing member declaration'; + if (!isSdsFunction(receiver.member.target.ref)) return 'MemberAccess: Member is not a function'; + if (!isSdsCall(receiver.receiver) && !isSdsReference(receiver.receiver)) + return `MemberAccess: Receiver is not a Reference or Call but - ${receiver.receiver.$type}`; + if ( + isSdsReference(receiver.receiver) && + !isSdsClass(receiver.receiver.target.ref) && + isSdsReference(receiver.receiver) && + !isSdsPlaceholder(receiver.receiver.target.ref) + ) + return 'MemberAccess: Reference Receiver is not Class of Placeholder'; + } + if (isSdsReference(receiver)) { + if (!isSdsClass(receiver.target.ref) && !isSdsSegment(receiver.target.ref)) + return 'Reference: Not a class or segment'; + } + + return receiver.$type; +}; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts new file mode 100644 index 000000000..1e3d3b405 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/edge.ts @@ -0,0 +1,55 @@ +import { GenericExpression } from './expression.js'; +import { Parameter } from './parameter.js'; +import { Parser } from './parser.js'; +import { Placeholder } from './placeholder.js'; +import { Result } from './result.js'; +import { SegmentGroupId } from './segment.js'; + +export class Edge { + public constructor( + public readonly from: Port, + public readonly to: Port, + ) {} + + public static create(from: Port, to: Port, parser: Parser) { + parser.graph.edgeList.push(new Edge(from, to)); + } +} + +export class Port { + private constructor( + public readonly nodeId: string, + public readonly portIdentifier: string, + ) {} + + public static fromName = (nodeId: number, name: string): Port => { + return new Port(nodeId.toString(), name); + }; + + public static fromPlaceholder = (placeholder: Placeholder, input: boolean): Port => { + return new Port(placeholder.name, input ? 'target' : 'source'); + }; + + public static fromResult = (result: Result, nodeId: number): Port => { + return new Port(nodeId.toString(), result.name); + }; + + public static fromParameter = (parameter: Parameter, nodeId: number): Port => { + return new Port(nodeId.toString(), parameter.name); + }; + + public static fromGenericExpression(node: GenericExpression, input: boolean) { + return new Port(node.id.toString(), input ? 'target' : 'source'); + } + + public static fromAssignee = (node: Placeholder | Result, input: boolean): Port => { + if (node instanceof Placeholder) { + return new Port(node.name, input ? 'target' : 'source'); + } + return new Port(SegmentGroupId.toString(), node.name); + }; + + public static isPortList(object: any): object is Port[] { + return Array.isArray(object) && object.every((element) => element instanceof Port); + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts new file mode 100644 index 000000000..3e121682f --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts @@ -0,0 +1,53 @@ +import { AstUtils } from 'langium'; +import { SdsExpression, isSdsCall, isSdsParameter, isSdsPlaceholder, isSdsReference } from '../../generated/ast.js'; +import { Call } from './call.js'; +import { Edge, Port } from './edge.js'; +import { Placeholder } from './placeholder.js'; +import { Parameter } from './parameter.js'; +import { Parser } from './parser.js'; + +export class GenericExpression { + public constructor( + public readonly id: number, + public readonly text: string, + public readonly type: string, + public readonly uniquePath: string, + ) {} +} + +export class Expression { + public static parse(node: SdsExpression, parser: Parser) { + if (isSdsCall(node)) return Call.parse(node, parser); + + if (isSdsReference(node) && isSdsPlaceholder(node.target.ref)) { + return Placeholder.parse(node.target.ref, parser); + } + if (isSdsReference(node) && isSdsParameter(node.target.ref)) { + return Parameter.parse(node.target.ref, parser); + } + + if (!node.$cstNode) return parser.pushError('Missing CstNode', node); + + const id = parser.getNewId(); + const genericExpression = new GenericExpression( + id, + node.$cstNode.text, + parser.computeType(node).toString(), + parser.getUniquePath(node), + ); + + const children = AstUtils.streamAst(node).iterator(); + for (const child of children) { + if (isSdsPlaceholder(child)) { + Edge.create( + Port.fromPlaceholder(Placeholder.parse(child, parser), false), + Port.fromGenericExpression(genericExpression, true), + parser, + ); + } + } + + parser.graph.genericExpressionList.push(genericExpression); + return genericExpression; + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parameter.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parameter.ts new file mode 100644 index 000000000..9044b09f8 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parameter.ts @@ -0,0 +1,24 @@ +import { SdsParameter } from '../../generated/ast.js'; +import { Parser } from './parser.js'; + +export class Parameter { + private constructor( + public readonly name: string, + public readonly isConstant: boolean, + public readonly type: string, + public argumentText?: string, + public readonly defaultValue?: string, + ) {} + + public static parse(node: SdsParameter, parser: Parser) { + const name = node.name; + const isConstant = node.isConstant; + + if (!node.type) return parser.pushError('Undefined Type', node); + const type = parser.computeType(node).toString(); + + const defaultValue = node.defaultValue?.$cstNode?.text; + + return new Parameter(name, isConstant, type, undefined, defaultValue); + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts new file mode 100644 index 000000000..3a8a259dc --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts @@ -0,0 +1,150 @@ +import { ILexingError, IRecognitionException } from 'chevrotain'; +import { URI, AstNode, LangiumDocument, AstNodeLocator } from 'langium'; +import { SafeDsLogger } from '../../communication/safe-ds-messaging-provider.js'; +import { CustomError, Graph, Segment } from '../global.js'; +import { SdsModule, isSdsPipeline, SdsStatement, isSdsSegment, SdsAnnotatedObject } from '../../generated/ast.js'; +import { documentToJson, saveJson } from './tools/debug-utils.js'; +import { Statement } from './statement.js'; +import { SafeDsAnnotations } from '../../builtins/safe-ds-annotations.js'; +import { SafeDsTypeComputer } from '../../typing/safe-ds-type-computer.js'; + +export class Parser { + private lastId: number; + private readonly logger?: SafeDsLogger; + private readonly documentUri: URI; + private errorList: CustomError[]; + public graph: Graph; + private AstNodeLocator: AstNodeLocator; + private Annotations: SafeDsAnnotations; + private TypeComputer: SafeDsTypeComputer; + + public constructor( + documentUri: URI, + graphType: 'pipeline' | 'segment', + Annotations: SafeDsAnnotations, + astNodeLocator: AstNodeLocator, + typeComputer: SafeDsTypeComputer, + logger?: SafeDsLogger, + lastId?: number, + ) { + this.errorList = []; + this.documentUri = documentUri; + this.graph = new Graph(graphType); + this.lastId = lastId ?? 0; + this.logger = logger; + this.Annotations = Annotations; + this.AstNodeLocator = astNodeLocator; + this.TypeComputer = typeComputer; + } + + public getNewId() { + return this.lastId++; + } + + public hasErrors() { + return this.errorList.length > 0; + } + + public getUniquePath(node: AstNode) { + return this.AstNodeLocator.getAstNodePath(node); + } + + public getCategory(node: SdsAnnotatedObject) { + return this.Annotations.getCategory(node); + } + + public computeType(node: AstNode) { + return this.TypeComputer.computeType(node); + } + + public pushError(message: string, origin?: AstNode) { + const error = new CustomError('block', this.constructErrorMessage(message, origin)); + this.errorList.push(error); + this.logger?.error(message); + return error; + } + + private constructErrorMessage(message: string, origin?: AstNode) { + const uri = origin?.$cstNode?.root.astNode.$document?.uri.fsPath ?? ''; + const position = origin?.$cstNode + ? `:${origin.$cstNode.range.start.line + 1}:${origin.$cstNode.range.start.character + 1}` + : ''; + + return `${uri}${position} - ${message}`; + } + + public pushLexerErrors(error: ILexingError) { + const uri = this.documentUri.toString(); + const position = error.line && error.column ? `:${error.line + 1}:${error.column + 1}` : ''; + + const message = `${uri}${position} - Lexer Error: ${error.message}`; + const fullError = `${uri}${position} - ${message}`; + + this.pushError(fullError); + } + + public pushParserErrors(error: IRecognitionException) { + const uri = this.documentUri.toString(); + const position = + error.token.startLine && error.token.startColumn + ? `:${error.token.startLine + 1}:${error.token.startColumn + 1}` + : ''; + + const message = `${uri}${position} - Parser Error: ${error.message}`; + const fullError = `${uri}${position} - ${message}`; + + this.pushError(fullError); + } + + public getResult() { + return { graph: this.graph, errorList: this.errorList }; + } + + public parsePipeline(document: LangiumDocument, debug: boolean = false) { + if (debug) { + // Creates a text document, that contains the json representation of the ast + saveJson(documentToJson(document, 16), document.uri); + } + + const root = document.parseResult.value as SdsModule; + const pipelines = root.members.filter((member) => isSdsPipeline(member)); + + if (pipelines.length !== 1) { + this.pushError('Pipeline must be defined exactly once'); + return; + } + const pipeline = pipelines[0]!; + const block = pipeline.body; + const statementList: SdsStatement[] = block.statements; + statementList.forEach((statement) => Statement.parse(statement, this)); + + this.graph.uniquePath = this.getUniquePath(pipeline); + this.graph.name = pipeline.name; + } + + public static parseSegments( + document: LangiumDocument, + Annotations: SafeDsAnnotations, + astNodeLocator: AstNodeLocator, + typeComputer: SafeDsTypeComputer, + logger?: SafeDsLogger, + ) { + const root = document.parseResult.value as SdsModule; + const segmentListRaw = root.members.filter((member) => isSdsSegment(member)); + + const segmentListParsed = segmentListRaw.map((segment) => { + const segmentParser = new Parser( + document.uri, + 'segment', + Annotations, + astNodeLocator, + typeComputer, + logger, + ); + return Segment.parse(segment, segmentParser); + }); + const segmentList = segmentListParsed.map((element) => element.segment); + const errorList = segmentListParsed.map((element) => element.errorList).flat(); + return { segmentList, errorList }; + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/placeholder.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/placeholder.ts new file mode 100644 index 000000000..71c04aabf --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/placeholder.ts @@ -0,0 +1,19 @@ +import { SdsPlaceholder } from '../../generated/ast.js'; +import { Parser } from './parser.js'; + +export class Placeholder { + private constructor( + public readonly name: string, + public type: string, + public readonly uniquePath: string, + ) {} + + public static parse(node: SdsPlaceholder, parser: Parser) { + const match = parser.graph.placeholderList.find((placeholder) => placeholder.name === node.name); + if (match) return match; + + const placeholder = new Placeholder(node.name, parser.computeType(node).toString(), parser.getUniquePath(node)); + parser.graph.placeholderList.push(placeholder); + return placeholder; + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/result.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/result.ts new file mode 100644 index 000000000..7aa2aff55 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/result.ts @@ -0,0 +1,18 @@ +import { SdsResult } from '../../generated/ast.js'; +import { Parser } from './parser.js'; + +export class Result { + constructor( + public readonly name: string, + public type: string, + ) {} + + public static parse(node: SdsResult, parser: Parser) { + const name = node.name; + + if (!node.type) return parser.pushError('Undefined Type', node); + const type = parser.computeType(node).toString(); + + return new Result(name, type); + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts new file mode 100644 index 000000000..59fb027a6 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts @@ -0,0 +1,55 @@ +import { SdsSegment, SdsStatement } from '../../generated/ast.js'; +import { Call, CustomError, Edge, GenericExpression, Graph, Placeholder } from '../global.js'; +import { Parameter } from './parameter.js'; +import { Parser } from './parser.js'; +import { Result } from './result.js'; +import { Statement } from './statement.js'; +import { filterErrors } from './utils.js'; + +export const SegmentGroupId = -1; + +export class Segment extends Graph { + private constructor( + public readonly parameterList: Parameter[], + public readonly resultList: Result[], + uniquePath: string, + name: string, + placeholderList: Placeholder[], + callList: Call[], + genericExpressionList: GenericExpression[], + edgeList: Edge[], + ) { + super('segment', placeholderList, callList, genericExpressionList, edgeList, uniquePath, name); + } + + public static parse(node: SdsSegment, parser: Parser): { segment: Segment; errorList: CustomError[] } { + const name = node.name; + const uniquePath = parser.getUniquePath(node); + + const resultList = filterErrors((node.resultList?.results ?? []).map((result) => Result.parse(result, parser))); + const parameterList = filterErrors( + (node.parameterList?.parameters ?? []).map((parameter) => Parameter.parse(parameter, parser)), + ); + + const statementList: SdsStatement[] = node.body.statements; + statementList.forEach((statement) => { + Statement.parse(statement, parser); + }); + + const { graph, errorList } = parser.getResult(); + graph.uniquePath = uniquePath; + graph.name = name; + + const segment = new Segment( + parameterList, + resultList, + name, + uniquePath, + graph.placeholderList, + graph.callList, + graph.genericExpressionList, + graph.edgeList, + ); + return { segment, errorList }; + } +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts new file mode 100644 index 000000000..13b207e93 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts @@ -0,0 +1,106 @@ +import { + SdsAssignee, + SdsStatement, + isSdsAssignment, + isSdsExpressionStatement, + isSdsPlaceholder, + isSdsWildcard, + isSdsYield, +} from '../../generated/ast.js'; +import { CustomError } from '../global.js'; +import { Call } from './call.js'; +import { Edge, Port } from './edge.js'; +import { Expression, GenericExpression } from './expression.js'; +import { Parameter } from './parameter.js'; +import { Placeholder } from './placeholder.js'; +import { Result } from './result.js'; +import { SegmentGroupId } from './segment.js'; +import { zip } from './utils.js'; +import { Parser } from './parser.js'; + +export class Statement { + public static parse(node: SdsStatement, parser: Parser) { + if (isSdsAssignment(node)) { + if (!node.assigneeList || node.assigneeList.assignees.length < 1) { + parser.pushError('Assignee(s) missing', node); + return; + } + const assigneeList = node.assigneeList.assignees.map((assignee) => Assignee.parse(assignee, parser)); + if (!containsNoErrors(assigneeList)) { + return; + } + + if (!node.expression) { + parser.pushError('Expression missing', node); + return; + } + const expression = Expression.parse(node.expression, parser); + if (expression instanceof CustomError) return; + + if (expression instanceof Call) { + if (assigneeList.length > expression.resultList.length) { + parser.pushError('Result(s) missing', node.expression); + } + if (assigneeList.length < expression.resultList.length) { + parser.pushError('Assignee(s) missing', node.assigneeList); + } + + zip(expression.resultList, assigneeList).forEach(([result, assignee]) => { + if (!assignee) return; + Edge.create(Port.fromResult(result, expression.id), Port.fromAssignee(assignee, true), parser); + assignee.type = result.type; + }); + } + if (expression instanceof GenericExpression) { + if (assigneeList.length > 1) { + parser.pushError('To many assignees', node.assigneeList); + return; + } + const assignee = assigneeList[0]!; + Edge.create(Port.fromGenericExpression(expression, false), Port.fromAssignee(assignee, true), parser); + assignee.type = expression.type; + } + if (expression instanceof Placeholder) { + if (assigneeList.length > 1) { + parser.pushError('To many assignees', node.assigneeList); + return; + } + const assignee = assigneeList[0]!; + Edge.create(Port.fromPlaceholder(expression, false), Port.fromAssignee(assignee, true), parser); + assignee.type = expression.type; + } + if (expression instanceof Parameter) { + if (assigneeList.length > 1) { + parser.pushError('To many assignees', node.assigneeList); + return; + } + const assignee = assigneeList[0]!; + Edge.create(Port.fromParameter(expression, SegmentGroupId), Port.fromAssignee(assignee, true), parser); + assignee.type = expression.type; + } + } + + if (isSdsExpressionStatement(node)) { + Expression.parse(node.expression, parser); + } + + return; + } +} + +const Assignee = { + parse(node: SdsAssignee, parser: Parser) { + if (isSdsPlaceholder(node)) return Placeholder.parse(node, parser); + + if (isSdsYield(node) && (!node.result || !node.result.ref)) return parser.pushError('Missing assignee', node); + if (isSdsYield(node)) return Result.parse(node.result!.ref!, parser); + + if (isSdsWildcard(node)) return; + + return parser.pushError(`Invalid assignee <${node.$type}>`, node); + }, +}; + +const containsNoErrors = (array: (T | CustomError)[]): array is T[] => { + return !array.some((element) => element instanceof CustomError); +}; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/tools/debug-utils.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/tools/debug-utils.ts new file mode 100644 index 000000000..f72b59856 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/tools/debug-utils.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-console */ +import { AstNode, LangiumDocument, URI, isAstNode, isReference } from 'langium'; +import { SafeDsAstReflection, isSdsAnnotationCallList } from '../../../generated/ast.js'; +import { writeFileSync } from 'fs'; + +export const printJson = (json: {}) => { + console.dir(json); +}; + +export const saveJson = (json: {}, documentPath: URI) => { + const extension = '.txt'; + const basePath = documentPath.fsPath.split('.')[0]; + const path = `${basePath}_debug${extension}`; + + try { + writeFileSync(path, JSON.stringify(json)); + console.log(`Debug: Saved Json`); + } catch (error) { + if (error instanceof Error) console.dir(error); + } +}; + +export const documentToJson = (document: LangiumDocument, depth: number): {} => { + const root = document.parseResult.value; + return nodeToJson(root, depth); +}; + +export const nodeToJson = (node: AstNode, depth: number): {} => { + // console.log(node.$type); + + const astHelper = new SafeDsAstReflection(); + const metadata = astHelper.getTypeMetaData(node.$type); + const result: { [key: string]: any } = { $type: metadata.name }; + + if (depth === 0) { + metadata.properties.forEach((property) => { + result[property.name] = 'DEPTH_STOP'; + }); + return result; + } + + metadata.properties.forEach((property) => { + const element = (node as any)[property.name] ?? property.defaultValue ?? ''; + + let parsedElement; + if (isSdsAnnotationCallList(element)) { + parsedElement = nodeToJson(element, depth - 1); + } else if (isAstNode(element)) { + parsedElement = nodeToJson(element, depth - 1); + } else if (Array.isArray(element)) { + parsedElement = element.map((listElement) => { + return nodeToJson(listElement, depth - 1); + }); + } else if (isReference(element)) { + parsedElement = { + ref: element.ref ? nodeToJson(element.ref, depth - 1) : '', + }; + } else if (typeof element === 'bigint') { + parsedElement = element.toString(); + } else { + parsedElement = element; + } + + result[property.name] = parsedElement; + }); + + return result; +}; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts new file mode 100644 index 000000000..d53beb879 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts @@ -0,0 +1,18 @@ +import { CustomError } from '../global.js'; + +export const zip = (arrayA: A[], arrayB: B[]): [A, B][] => { + const minLength = Math.min(arrayA.length, arrayB.length); + const result: [A, B][] = []; + + for (let i = 0; i < minLength; i++) { + result.push([arrayA[i]!, arrayB[i]!]); + } + + return result; +}; + +export const filterErrors = (array: (T | CustomError)[]): T[] => { + return array.filter( + (element): element is Exclude => !(element instanceof CustomError), + ); +}; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/global.ts b/packages/safe-ds-lang/src/language/graphical-editor/global.ts new file mode 100644 index 000000000..b3bdf9ec2 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/global.ts @@ -0,0 +1,54 @@ +import { Call } from './ast-parser/call.js'; +import { GenericExpression } from './ast-parser/expression.js'; +import { Edge } from './ast-parser/edge.js'; +import { Placeholder } from './ast-parser/placeholder.js'; +import { Segment } from './ast-parser/segment.js'; + +export { SegmentGroupId } from './ast-parser/segment.js'; +export { Segment } from './ast-parser/segment.js'; +export { Placeholder } from './ast-parser/placeholder.js'; +export { Call } from './ast-parser/call.js'; +export { GenericExpression } from './ast-parser/expression.js'; +export { Edge } from './ast-parser/edge.js'; +export { Parameter } from './ast-parser/parameter.js'; +export { Result } from './ast-parser/result.js'; + +export interface Collection { + pipeline: Graph; + segmentList: Segment[]; + errorList: CustomError[]; +} + +export class Graph { + constructor( + public readonly type: 'segment' | 'pipeline', + public readonly placeholderList: Placeholder[] = [], + public readonly callList: Call[] = [], + public readonly genericExpressionList: GenericExpression[] = [], + public readonly edgeList: Edge[] = [], + public uniquePath: string = '', + public name: string = '', + ) {} +} + +export class Buildin { + constructor( + public readonly name: string, + public readonly parent: string | undefined, + public readonly category: + | 'DataImport' + | 'DataExport' + | 'DataProcessing' + | 'DataExploration' + | 'Modeling' + | 'ModelEvaluation' + | (string & Record), + ) {} +} + +export class CustomError { + constructor( + public readonly action: 'block' | 'notify', + public readonly message: string, + ) {} +} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts b/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts new file mode 100644 index 000000000..2e6211b97 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts @@ -0,0 +1,253 @@ +import { SafeDsLogger, SafeDsMessagingProvider } from '../communication/safe-ds-messaging-provider.js'; +import { SafeDsServices } from '../safe-ds-module.js'; +import { Uri } from 'vscode'; +import { extname } from 'path'; +import { + AstNodeLocator, + DocumentationProvider, + DocumentBuilder, + DocumentState, + LangiumDocuments, + Disposable, + URI, + IndexManager, +} from 'langium'; +import { + isSdsAnnotation, + isSdsCall, + isSdsClass, + isSdsEnum, + isSdsFunction, + isSdsMemberAccess, + isSdsReference, + isSdsSegment, +} from '../generated/ast.js'; +import { Connection, DidSaveTextDocumentParams } from 'vscode-languageserver'; +import { Buildin, Collection } from './global.js'; +import { + GraphicalEditorCloseSyncChannelRequest, + GraphicalEditorGetBuildinsRequest, + GraphicalEditorGetDocumentationRequest, + GraphicalEditorOpenSyncChannelRequest, + GraphicalEditorParseDocumentRequest, + GraphicalEditorSyncEventNotification, +} from '../communication/rpc.js'; +import { isPrivate } from '../helpers/nodeProperties.js'; +import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; +import { Parser } from './ast-parser/parser.js'; +import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; + +export class SafeDsGraphicalEditorProvider { + private readonly logger: SafeDsLogger; + private readonly LangiumDocuments: LangiumDocuments; + private readonly DocumentBuilder: DocumentBuilder; + private readonly AstNodeLocator: AstNodeLocator; + private readonly DocProvider: DocumentationProvider; + private readonly MessagingProvider: SafeDsMessagingProvider; + private readonly IndexManager: IndexManager; + private readonly Annotations: SafeDsAnnotations; + private readonly TypeComputer: SafeDsTypeComputer; + private readonly connection: Connection | undefined; + + private readonly SYNC_TRIGGER_STATE: DocumentState = 6; + private readonly openChannel = new Map(); + + constructor(services: SafeDsServices) { + this.logger = services.communication.MessagingProvider.createTaggedLogger('Graphical Editor'); + this.LangiumDocuments = services.shared.workspace.LangiumDocuments; + this.DocumentBuilder = services.shared.workspace.DocumentBuilder; + this.AstNodeLocator = services.workspace.AstNodeLocator; + this.DocProvider = services.documentation.DocumentationProvider; + this.MessagingProvider = services.communication.MessagingProvider; + this.IndexManager = services.shared.workspace.IndexManager; + this.Annotations = services.builtins.Annotations; + this.TypeComputer = services.typing.TypeComputer; + this.connection = services.shared.lsp.Connection; + + this.MessagingProvider.onRequest(GraphicalEditorParseDocumentRequest.type, this.parseDocument); + this.MessagingProvider.onRequest(GraphicalEditorGetDocumentationRequest.type, this.getDocumentation); + this.MessagingProvider.onRequest(GraphicalEditorOpenSyncChannelRequest.type, this.openSyncChannel); + this.MessagingProvider.onRequest(GraphicalEditorCloseSyncChannelRequest.type, this.closeSyncChannel); + this.MessagingProvider.onRequest(GraphicalEditorGetBuildinsRequest.type, this.getBuildins); + } + + public parseDocument = async (uri: Uri): Promise => { + const parser = new Parser( + uri, + 'pipeline', + this.Annotations, + this.AstNodeLocator, + this.TypeComputer, + this.logger, + ); + + const validTypes = ['.sds', '.sdsdev']; + + const fileType = extname(uri.path); + if (!validTypes.includes(fileType)) { + parser.pushError(`Unknown file type <${fileType}>`); + const { graph, errorList } = parser.getResult(); + return { pipeline: graph, errorList, segmentList: [] }; + } + + const document = await this.LangiumDocuments.getOrCreateDocument(uri); + await this.DocumentBuilder.build([document]); + + document.parseResult.lexerErrors.forEach(parser.pushLexerErrors); + document.parseResult.parserErrors.forEach(parser.pushParserErrors); + if (parser.hasErrors()) { + const { graph, errorList } = parser.getResult(); + return { pipeline: graph, errorList, segmentList: [] }; + } + + parser.parsePipeline(document); + const { graph: pipeline, errorList: errorListPipeline } = parser.getResult(); + + const { segmentList, errorList: errorListSegment } = Parser.parseSegments( + document, + this.Annotations, + this.AstNodeLocator, + this.TypeComputer, + this.logger, + ); + + const errorList = [...errorListPipeline, ...errorListSegment]; + + return { pipeline, errorList, segmentList }; + }; + + public async getDocumentation(params: { uri: Uri; uniquePath: string }): Promise { + const validTypes = ['.sds', '.sdsdev']; + + const fileType = extname(params.uri.path); + if (!validTypes.includes(fileType)) { + this.logger.error(`GetDocumentation: Unknown file type <${fileType}>`); + return; + } + + const document = await this.LangiumDocuments.getOrCreateDocument(params.uri); + await this.DocumentBuilder.build([document]); + + const root = document.parseResult.value; + const node = this.AstNodeLocator.getAstNode(root, params.uniquePath); + + if (!node) { + this.logger.error(`GetDocumentation: Node retrieval failed for <${params.uniquePath}>`); + return; + } + + if (!isSdsCall(node)) { + this.logger.error(`GetDocumentation: Invalid node type <${node.$type}>`); + return; + } + + const receiver = node.receiver; + if (isSdsMemberAccess(receiver)) { + const fun = receiver.member?.target.ref!; + return this.DocProvider.getDocumentation(fun); + } + + if (isSdsReference(receiver)) { + const cls = receiver.target.ref!; + return this.DocProvider.getDocumentation(cls); + } + + this.logger.error(`GetDocumentation: Invalid call receiver <${node.$type}>`); + return; + } + + public closeSyncChannel(uri: Uri) { + if (!this.openChannel.has(uri.toString())) return; + + const channel = this.openChannel.get(uri.toString())!; + channel.dispose(); + } + + public openSyncChannel(uri: Uri) { + if (!this.connection) { + this.logger.error('OpenSyncChannel: No connection to client'); + return; + } + + this.closeSyncChannel(uri); + + const syncEventHandler = async (params: DidSaveTextDocumentParams) => { + const documentUri = URI.parse(params.textDocument.uri); + const response = await this.parseDocument(documentUri); + + this.MessagingProvider.sendNotification(GraphicalEditorSyncEventNotification.type, response); + }; + + const channel = this.connection.onDidSaveTextDocument(syncEventHandler); + + /* + Man könnte über diese Methode ein Update des Graphen bei jedem Keystroke triggern. + Dieses Verhalten speziell ist vermutlich zu viel, aber eine debouncte Version könnte interessant sein. + + const syncHandler: DocumentBuildListener = () => { + const response: SyncChannelInterface.Response = { + test: "THIS is a sync event", + }; + connection.sendNotification(SyncChannelHandler.method, response); + }; + + sharedServices.workspace.DocumentBuilder.onBuildPhase( + SYNC_TRIGGER_STATE, + syncHandler, + ); + */ + + this.openChannel.set(uri.toString(), channel); + } + + public async getBuildins(): Promise { + const resultList: Buildin[] = []; + const allElements = this.IndexManager.allElements(); + + for (const element of allElements) { + if (!element.node) { + this.logger.warn(`GetBuildins: Unable to parse <${element.name}>`); + continue; + } + + if (isSdsClass(element.node)) { + const name = element.node.name; + + const classMemberList = element.node.body?.members ?? []; + const functionList: Buildin[] = classMemberList + .filter((member) => isSdsFunction(member)) + .filter((fun) => !isPrivate(fun)) + .map((fun) => { + const category = this.Annotations.getCategory(fun); + return { + category: category?.name ?? '', + name: fun.name, + parent: name, + }; + }); + resultList.push(...functionList); + } + + if (isSdsFunction(element.node)) { + resultList.push({ + name: element.node.name, + category: this.Annotations.getCategory(element.node)?.name ?? '', + parent: undefined, + }); + } + + if (isSdsSegment(element.node)) { + continue; + } + + if (isSdsAnnotation(element.node) || isSdsEnum(element.node)) { + this.logger.info(`GetBuildins: Skipping <${element.node.$type}>`); + continue; + } + + this.logger.warn(`GetBuildins: Unable to parse <${element.node.$type}>`); + } + + return resultList; + } +} diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts index 66b455228..4b9991101 100644 --- a/packages/safe-ds-lang/src/language/safe-ds-module.ts +++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts @@ -59,6 +59,7 @@ import { SafeDsSyntheticProperties } from './helpers/safe-ds-synthetic-propertie import { SafeDsLinker } from './scoping/safe-ds-linker.js'; import { SafeDsCodeActionProvider } from './codeActions/safe-ds-code-action-provider.js'; import { SafeDsQuickfixProvider } from './codeActions/quickfixes/safe-ds-quickfix-provider.js'; +import { SafeDsGraphicalEditorProvider } from './graphical-editor/graphical-editor-provider.js'; /** * Declaration of custom services - add your own service classes here. @@ -115,6 +116,9 @@ export type SafeDsAddedServices = { PackageManager: SafeDsPackageManager; SettingsProvider: SafeDsSettingsProvider; }; + graphicalEditor: { + GraphicalEditorProvider: SafeDsGraphicalEditorProvider; + }; }; export type SafeDsAddedSharedServices = { @@ -209,6 +213,9 @@ export const SafeDsModule: Module new SafeDsPackageManager(services), SettingsProvider: (services) => new SafeDsSettingsProvider(services), }, + graphicalEditor: { + GraphicalEditorProvider: (services) => new SafeDsGraphicalEditorProvider(services), + }, }; export const SafeDsSharedModule: Module> = { From 59df6f3c04e5ecb6ddc6002f9710bf413e9065fb Mon Sep 17 00:00:00 2001 From: Gideon Koenig Date: Tue, 21 Jan 2025 21:59:26 +0100 Subject: [PATCH 2/4] fix: type issue --- .../src/language/graphical-editor/ast-parser/parser.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts index 3a8a259dc..3e9372f5b 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts @@ -116,7 +116,9 @@ export class Parser { const pipeline = pipelines[0]!; const block = pipeline.body; const statementList: SdsStatement[] = block.statements; - statementList.forEach((statement) => Statement.parse(statement, this)); + statementList.forEach((statement) => { + Statement.parse(statement, this); + }); this.graph.uniquePath = this.getUniquePath(pipeline); this.graph.name = pipeline.name; From a0cc4ec9c74a6dcd228edf22e5c13aba6636beae Mon Sep 17 00:00:00 2001 From: Gideon Koenig Date: Tue, 21 Jan 2025 22:53:57 +0100 Subject: [PATCH 3/4] fix: circular imports --- .../graphical-editor/ast-parser/argument.ts | 2 +- .../graphical-editor/ast-parser/call.ts | 2 +- .../graphical-editor/ast-parser/parser.ts | 3 +- .../graphical-editor/ast-parser/segment.ts | 6 ++- .../graphical-editor/ast-parser/statement.ts | 2 +- .../graphical-editor/ast-parser/utils.ts | 2 +- .../src/language/graphical-editor/global.ts | 40 +------------------ .../graphical-editor-provider.ts | 5 ++- .../src/language/graphical-editor/types.ts | 38 ++++++++++++++++++ 9 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 packages/safe-ds-lang/src/language/graphical-editor/types.ts diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts index 0f7e226c8..f440f6937 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/argument.ts @@ -1,10 +1,10 @@ import { isSdsLiteral, SdsArgument } from '../../generated/ast.js'; -import { CustomError } from '../global.js'; import { Call } from './call.js'; import { Placeholder } from './placeholder.js'; import { Expression, GenericExpression } from './expression.js'; import { Parameter } from './parameter.js'; import { Parser } from './parser.js'; +import { CustomError } from '../types.js'; export class Argument { constructor( diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts index c50c202d2..307a71e10 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/call.ts @@ -15,7 +15,6 @@ import { isSdsReference, isSdsSegment, } from '../../generated/ast.js'; -import { CustomError } from '../global.js'; import { Argument } from './argument.js'; import { Edge, Port } from './edge.js'; import { GenericExpression } from './expression.js'; @@ -24,6 +23,7 @@ import { Placeholder } from './placeholder.js'; import { Result } from './result.js'; import { filterErrors } from './utils.js'; import { Parser } from './parser.js'; +import { CustomError } from '../types.js'; export class Call { private constructor( diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts index 3e9372f5b..c1850e426 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/parser.ts @@ -1,12 +1,13 @@ import { ILexingError, IRecognitionException } from 'chevrotain'; import { URI, AstNode, LangiumDocument, AstNodeLocator } from 'langium'; import { SafeDsLogger } from '../../communication/safe-ds-messaging-provider.js'; -import { CustomError, Graph, Segment } from '../global.js'; import { SdsModule, isSdsPipeline, SdsStatement, isSdsSegment, SdsAnnotatedObject } from '../../generated/ast.js'; import { documentToJson, saveJson } from './tools/debug-utils.js'; import { Statement } from './statement.js'; import { SafeDsAnnotations } from '../../builtins/safe-ds-annotations.js'; import { SafeDsTypeComputer } from '../../typing/safe-ds-type-computer.js'; +import { CustomError, Graph } from '../types.js'; +import { Segment } from './segment.js'; export class Parser { private lastId: number; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts index 59fb027a6..5389cd683 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/segment.ts @@ -1,7 +1,11 @@ import { SdsSegment, SdsStatement } from '../../generated/ast.js'; -import { Call, CustomError, Edge, GenericExpression, Graph, Placeholder } from '../global.js'; +import { CustomError, Graph } from '../types.js'; +import { Call } from './call.js'; +import { Edge } from './edge.js'; +import { GenericExpression } from './expression.js'; import { Parameter } from './parameter.js'; import { Parser } from './parser.js'; +import { Placeholder } from './placeholder.js'; import { Result } from './result.js'; import { Statement } from './statement.js'; import { filterErrors } from './utils.js'; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts index 13b207e93..418dd8499 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/statement.ts @@ -7,7 +7,6 @@ import { isSdsWildcard, isSdsYield, } from '../../generated/ast.js'; -import { CustomError } from '../global.js'; import { Call } from './call.js'; import { Edge, Port } from './edge.js'; import { Expression, GenericExpression } from './expression.js'; @@ -17,6 +16,7 @@ import { Result } from './result.js'; import { SegmentGroupId } from './segment.js'; import { zip } from './utils.js'; import { Parser } from './parser.js'; +import { CustomError } from '../types.js'; export class Statement { public static parse(node: SdsStatement, parser: Parser) { diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts index d53beb879..595150342 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/utils.ts @@ -1,4 +1,4 @@ -import { CustomError } from '../global.js'; +import { CustomError } from '../types.js'; export const zip = (arrayA: A[], arrayB: B[]): [A, B][] => { const minLength = Math.min(arrayA.length, arrayB.length); diff --git a/packages/safe-ds-lang/src/language/graphical-editor/global.ts b/packages/safe-ds-lang/src/language/graphical-editor/global.ts index b3bdf9ec2..11ca3bd0a 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/global.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/global.ts @@ -1,8 +1,5 @@ -import { Call } from './ast-parser/call.js'; -import { GenericExpression } from './ast-parser/expression.js'; -import { Edge } from './ast-parser/edge.js'; -import { Placeholder } from './ast-parser/placeholder.js'; import { Segment } from './ast-parser/segment.js'; +import { Graph, CustomError } from './types.js'; export { SegmentGroupId } from './ast-parser/segment.js'; export { Segment } from './ast-parser/segment.js'; @@ -12,43 +9,10 @@ export { GenericExpression } from './ast-parser/expression.js'; export { Edge } from './ast-parser/edge.js'; export { Parameter } from './ast-parser/parameter.js'; export { Result } from './ast-parser/result.js'; +export { Graph, Buildin, CustomError } from './types.js'; export interface Collection { pipeline: Graph; segmentList: Segment[]; errorList: CustomError[]; } - -export class Graph { - constructor( - public readonly type: 'segment' | 'pipeline', - public readonly placeholderList: Placeholder[] = [], - public readonly callList: Call[] = [], - public readonly genericExpressionList: GenericExpression[] = [], - public readonly edgeList: Edge[] = [], - public uniquePath: string = '', - public name: string = '', - ) {} -} - -export class Buildin { - constructor( - public readonly name: string, - public readonly parent: string | undefined, - public readonly category: - | 'DataImport' - | 'DataExport' - | 'DataProcessing' - | 'DataExploration' - | 'Modeling' - | 'ModelEvaluation' - | (string & Record), - ) {} -} - -export class CustomError { - constructor( - public readonly action: 'block' | 'notify', - public readonly message: string, - ) {} -} diff --git a/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts b/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts index 2e6211b97..53238d77c 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/graphical-editor-provider.ts @@ -1,5 +1,5 @@ import { SafeDsLogger, SafeDsMessagingProvider } from '../communication/safe-ds-messaging-provider.js'; -import { SafeDsServices } from '../safe-ds-module.js'; +import { type SafeDsServices } from '../safe-ds-module.js'; import { Uri } from 'vscode'; import { extname } from 'path'; import { @@ -23,7 +23,7 @@ import { isSdsSegment, } from '../generated/ast.js'; import { Connection, DidSaveTextDocumentParams } from 'vscode-languageserver'; -import { Buildin, Collection } from './global.js'; +import { Collection } from './global.js'; import { GraphicalEditorCloseSyncChannelRequest, GraphicalEditorGetBuildinsRequest, @@ -36,6 +36,7 @@ import { isPrivate } from '../helpers/nodeProperties.js'; import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; import { Parser } from './ast-parser/parser.js'; import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; +import { Buildin } from './types.js'; export class SafeDsGraphicalEditorProvider { private readonly logger: SafeDsLogger; diff --git a/packages/safe-ds-lang/src/language/graphical-editor/types.ts b/packages/safe-ds-lang/src/language/graphical-editor/types.ts new file mode 100644 index 000000000..71132d429 --- /dev/null +++ b/packages/safe-ds-lang/src/language/graphical-editor/types.ts @@ -0,0 +1,38 @@ +import { Call } from './ast-parser/call.js'; +import { Edge } from './ast-parser/edge.js'; +import { GenericExpression } from './ast-parser/expression.js'; +import { Placeholder } from './ast-parser/placeholder.js'; + +export class Graph { + constructor( + public readonly type: 'segment' | 'pipeline', + public readonly placeholderList: Placeholder[] = [], + public readonly callList: Call[] = [], + public readonly genericExpressionList: GenericExpression[] = [], + public readonly edgeList: Edge[] = [], + public uniquePath: string = '', + public name: string = '', + ) {} +} + +export class Buildin { + constructor( + public readonly name: string, + public readonly parent: string | undefined, + public readonly category: + | 'DataImport' + | 'DataExport' + | 'DataProcessing' + | 'DataExploration' + | 'Modeling' + | 'ModelEvaluation' + | (string & Record), + ) {} +} + +export class CustomError { + constructor( + public readonly action: 'block' | 'notify', + public readonly message: string, + ) {} +} From 50a458e06fe87d851041a4ce8ffbd08881c90b64 Mon Sep 17 00:00:00 2001 From: Gideon Koenig Date: Wed, 22 Jan 2025 00:41:52 +0100 Subject: [PATCH 4/4] fix: enum variants are not handled properly --- .../graphical-editor/ast-parser/expression.ts | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts index 3e121682f..09500509a 100644 --- a/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts +++ b/packages/safe-ds-lang/src/language/graphical-editor/ast-parser/expression.ts @@ -1,5 +1,19 @@ import { AstUtils } from 'langium'; -import { SdsExpression, isSdsCall, isSdsParameter, isSdsPlaceholder, isSdsReference } from '../../generated/ast.js'; +import { + SdsClass, + SdsEnum, + SdsEnumVariant, + SdsExpression, + SdsMemberAccess, + isSdsCall, + isSdsClass, + isSdsEnum, + isSdsEnumVariant, + isSdsMemberAccess, + isSdsParameter, + isSdsPlaceholder, + isSdsReference, +} from '../../generated/ast.js'; import { Call } from './call.js'; import { Edge, Port } from './edge.js'; import { Placeholder } from './placeholder.js'; @@ -17,7 +31,7 @@ export class GenericExpression { export class Expression { public static parse(node: SdsExpression, parser: Parser) { - if (isSdsCall(node)) return Call.parse(node, parser); + if (isSdsCall(node) && !isEnumVariant(node.receiver)) return Call.parse(node, parser); if (isSdsReference(node) && isSdsPlaceholder(node.target.ref)) { return Placeholder.parse(node.target.ref, parser); @@ -51,3 +65,38 @@ export class Expression { return genericExpression; } } + +type EnumVariantCall = SdsMemberAccess & { + member: { + target: { + ref: SdsEnumVariant; + }; + }; + receiver: SdsMemberAccess & { + member: { + target: { + ref: SdsEnum; + }; + }; + receiver: { + target: { + ref: SdsClass; + }; + }; + }; +}; + +const isEnumVariant = (receiver: SdsExpression): receiver is EnumVariantCall => { + /* eslint-disable no-implicit-coercion */ + return ( + isSdsMemberAccess(receiver) && + !!receiver.member && + !!receiver.member.target.ref && + isSdsEnumVariant(receiver.member.target.ref) && + isSdsMemberAccess(receiver.receiver) && + isSdsReference(receiver.receiver.member) && + isSdsEnum(receiver.receiver.member.target.ref) && + isSdsReference(receiver.receiver.receiver) && + isSdsClass(receiver.receiver.receiver.target.ref) + ); +};