diff --git a/src/cli/generator.ts b/src/cli/generator.ts index 59fe7b7ba..2d64c035a 100644 --- a/src/cli/generator.ts +++ b/src/cli/generator.ts @@ -18,6 +18,7 @@ import { isSdsEnumVariant, isSdsExpressionLambda, isSdsExpressionStatement, + isSdsFunction, isSdsIndexedAccess, isSdsInfixOperation, isSdsList, @@ -59,12 +60,12 @@ import { NodeFileSystem } from 'langium/node'; import { getAbstractResults, getAssignees, - streamBlockLambdaResults, getImportedDeclarations, getImports, - isRequiredParameter, getModuleMembers, getStatements, + isRequiredParameter, + streamBlockLambdaResults, } from '../language/helpers/nodeProperties.js'; import { IdManager } from '../language/helpers/idManager.js'; import { isInStubFile } from '../language/helpers/fileExtensions.js'; @@ -209,7 +210,7 @@ const generateParameter = function ( frame: GenerationInfoFrame, defaultValue: boolean = true, ): string { - return expandToString`${getPythonNameOrDefault(frame.getServices(), parameter)}${ + return expandToString`${getPythonNameOrDefault(frame.services, parameter)}${ defaultValue && parameter.defaultValue !== undefined ? '=' + generateExpression(parameter.defaultValue, frame) : '' @@ -291,7 +292,7 @@ const generateStatement = function (statement: SdsStatement, frame: GenerationIn const generateAssignment = function (assignment: SdsAssignment, frame: GenerationInfoFrame): string { const requiredAssignees = isSdsCall(assignment.expression) - ? getAbstractResults(frame.getServices().helpers.NodeMapper.callToCallable(assignment.expression)).length + ? getAbstractResults(frame.services.helpers.NodeMapper.callToCallable(assignment.expression)).length : /* c8 ignore next */ 1; const assignees = getAssignees(assignment); @@ -347,7 +348,7 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio } } - const partiallyEvaluatedNode = frame.getServices().evaluation.PartialEvaluator.evaluate(expression); + const partiallyEvaluatedNode = frame.services.evaluation.PartialEvaluator.evaluate(expression); if (partiallyEvaluatedNode instanceof BooleanConstant) { return partiallyEvaluatedNode.value ? 'True' : 'False'; } else if (partiallyEvaluatedNode instanceof IntConstant) { @@ -360,39 +361,44 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio } else if (partiallyEvaluatedNode instanceof StringConstant) { return `'${formatStringSingleLine(partiallyEvaluatedNode.value)}'`; } - // Handled after constant expressions: EnumVariant, List, Map - if (isSdsTemplateString(expression)) { + // Handled after constant expressions: EnumVariant, List, Map + else if (isSdsTemplateString(expression)) { return `f'${expression.expressions.map((expr) => generateExpression(expr, frame)).join('')}'`; - } - - if (isSdsMap(expression)) { + } else if (isSdsMap(expression)) { const mapContent = expression.entries.map( (entry) => `${generateExpression(entry.key, frame)}: ${generateExpression(entry.value, frame)}`, ); return `{${mapContent.join(', ')}}`; - } - if (isSdsList(expression)) { + } else if (isSdsList(expression)) { const listContent = expression.elements.map((value) => generateExpression(value, frame)); return `[${listContent.join(', ')}]`; - } - - if (isSdsBlockLambda(expression)) { + } else if (isSdsBlockLambda(expression)) { return frame.getUniqueLambdaBlockName(expression); - } - if (isSdsCall(expression)) { - const sortedArgs = sortArguments(frame.getServices(), expression.argumentList.arguments); + } else if (isSdsCall(expression)) { + const callable = frame.services.helpers.NodeMapper.callToCallable(expression); + if (isSdsFunction(callable)) { + const pythonCall = frame.services.builtins.Annotations.getPythonCall(callable); + if (pythonCall) { + let thisParam: string | undefined = undefined; + if (isSdsMemberAccess(expression.receiver)) { + thisParam = generateExpression(expression.receiver.receiver, frame); + } + const argumentsMap = getArgumentsMap(expression.argumentList.arguments, frame); + return generatePythonCall(pythonCall, argumentsMap, thisParam); + } + } + + const sortedArgs = sortArguments(frame.services, expression.argumentList.arguments); return expandToString`${generateExpression(expression.receiver, frame)}(${sortedArgs .map((arg) => generateArgument(arg, frame)) .join(', ')})`; - } - if (isSdsExpressionLambda(expression)) { + } else if (isSdsExpressionLambda(expression)) { return `lambda ${generateParameters(expression.parameterList, frame)}: ${generateExpression( expression.result, frame, )}`; - } - if (isSdsInfixOperation(expression)) { + } else if (isSdsInfixOperation(expression)) { const leftOperand = generateExpression(expression.leftOperand, frame); const rightOperand = generateExpression(expression.rightOperand, frame); switch (expression.operator) { @@ -412,14 +418,12 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio default: return `(${leftOperand}) ${expression.operator} (${rightOperand})`; } - } - if (isSdsIndexedAccess(expression)) { + } else if (isSdsIndexedAccess(expression)) { return expandToString`${generateExpression(expression.receiver, frame)}[${generateExpression( expression.index, frame, )}]`; - } - if (isSdsMemberAccess(expression)) { + } else if (isSdsMemberAccess(expression)) { const member = expression.member?.target.ref!; const receiver = generateExpression(expression.receiver, frame); if (isSdsEnumVariant(member)) { @@ -442,11 +446,9 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio return `${receiver}.${memberExpression}`; } } - } - if (isSdsParenthesizedExpression(expression)) { + } else if (isSdsParenthesizedExpression(expression)) { return expandToString`${generateExpression(expression.expression, frame)}`; - } - if (isSdsPrefixOperation(expression)) { + } else if (isSdsPrefixOperation(expression)) { const operand = generateExpression(expression.operand, frame); switch (expression.operator) { case 'not': @@ -454,19 +456,39 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio case '-': return expandToString`-(${operand})`; } - } - if (isSdsReference(expression)) { + } else if (isSdsReference(expression)) { const declaration = expression.target.ref!; const referenceImport = - getExternalReferenceNeededImport(frame.getServices(), expression, declaration) || - getInternalReferenceNeededImport(frame.getServices(), expression, declaration); + getExternalReferenceNeededImport(frame.services, expression, declaration) || + getInternalReferenceNeededImport(frame.services, expression, declaration); frame.addImport(referenceImport); - return referenceImport?.alias || getPythonNameOrDefault(frame.getServices(), declaration); + return referenceImport?.alias || getPythonNameOrDefault(frame.services, declaration); } /* c8 ignore next 2 */ throw new Error(`Unknown expression type: ${expression.$type}`); }; +const generatePythonCall = function ( + pythonCall: string, + argumentsMap: Map, + thisParam: string | undefined = undefined, +): string { + if (thisParam) { + argumentsMap.set('this', thisParam); + } + + return pythonCall.replace(/\$[_a-zA-Z][_a-zA-Z0-9]*/gu, (value) => argumentsMap.get(value.substring(1))!); +}; + +const getArgumentsMap = function (argumentList: SdsArgument[], frame: GenerationInfoFrame): Map { + const argumentsMap = new Map(); + argumentList.reduce((map, value) => { + map.set(frame.services.helpers.NodeMapper.argumentToParameter(value)?.name!, generateArgument(value, frame)); + return map; + }, argumentsMap); + return argumentsMap; +}; + const sortArguments = function (services: SafeDsServices, argumentList: SdsArgument[]): SdsArgument[] { // $containerIndex contains the index of the parameter in the receivers parameter list const parameters = argumentList.map((argument) => { @@ -482,7 +504,7 @@ const sortArguments = function (services: SafeDsServices, argumentList: SdsArgum }; const generateArgument = function (argument: SdsArgument, frame: GenerationInfoFrame) { - const parameter = frame.getServices().helpers.NodeMapper.argumentToParameter(argument); + const parameter = frame.services.helpers.NodeMapper.argumentToParameter(argument); return expandToString`${ parameter !== undefined && !isRequiredParameter(parameter) ? generateParameter(parameter, frame, false) + '=' @@ -567,9 +589,9 @@ interface ImportData { } class GenerationInfoFrame { - services: SafeDsServices; - blockLambdaManager: IdManager; - importSet: Map; + readonly services: SafeDsServices; + private readonly blockLambdaManager: IdManager; + private readonly importSet: Map; constructor(services: SafeDsServices, importSet: Map = new Map()) { this.services = services; @@ -589,10 +611,6 @@ class GenerationInfoFrame { getUniqueLambdaBlockName(lambda: SdsBlockLambda): string { return `${BLOCK_LAMBDA_PREFIX}${this.blockLambdaManager.assignId(lambda)}`; } - - getServices(): SafeDsServices { - return this.services; - } } export interface GenerateOptions { diff --git a/src/language/builtins/safe-ds-annotations.ts b/src/language/builtins/safe-ds-annotations.ts index 74212c980..003690474 100644 --- a/src/language/builtins/safe-ds-annotations.ts +++ b/src/language/builtins/safe-ds-annotations.ts @@ -4,6 +4,7 @@ import { SdsAnnotatedObject, SdsAnnotation, SdsEnumVariant, + SdsFunction, SdsModule, SdsParameter, } from '../generated/ast.js'; @@ -65,6 +66,19 @@ export class SafeDsAnnotations extends SafeDsModuleMembers { return this.getAnnotation(IDE_INTEGRATION_URI, 'Expert'); } + getPythonCall(node: SdsFunction | undefined): string | undefined { + const value = this.getArgumentValue(node, this.PythonCall, 'callSpecification'); + if (value instanceof StringConstant) { + return value.value; + } else { + return undefined; + } + } + + get PythonCall(): SdsAnnotation | undefined { + return this.getAnnotation(CODE_GENERATION_URI, 'PythonCall'); + } + getPythonModule(node: SdsModule | undefined): string | undefined { const value = this.getArgumentValue(node, this.PythonModule, 'qualifiedName'); if (value instanceof StringConstant) { diff --git a/src/language/validation/names.ts b/src/language/validation/names.ts index 1b5b98fb5..a263d193e 100644 --- a/src/language/validation/names.ts +++ b/src/language/validation/names.ts @@ -235,7 +235,7 @@ export const moduleMemberMustHaveNameThatIsUniqueInPackage = (services: SafeDsSe let declarationsInPackage: AstNodeDescription[]; let kind: string; if (packageName.startsWith(BUILTINS_ROOT_PACKAGE)) { - // For a builtin package the simple names of declarations must be unique + // For a builtin package, the simple names of declarations must be unique declarationsInPackage = packageManager.getDeclarationsInPackageOrSubpackage(BUILTINS_ROOT_PACKAGE); kind = 'builtin declarations'; } else { diff --git a/src/resources/builtins/safeds/lang/codeGeneration.sdsstub b/src/resources/builtins/safeds/lang/codeGeneration.sdsstub index 83b09a0e1..5e204a00b 100644 --- a/src/resources/builtins/safeds/lang/codeGeneration.sdsstub +++ b/src/resources/builtins/safeds/lang/codeGeneration.sdsstub @@ -1,5 +1,19 @@ package safeds.lang +/** + * The specification of a corresponding function call in Python. By default, the function is called as specified in the + * stub. + * + * @param callSpecification + * The specification of corresponding Python call. The specification can contain template expression, which are + * replaced by the corresponding arguments of the function call. `$this` is replaced by the receiver of the call. + * `$param` is replaced by the value of the parameter called `param`. Otherwise, the string is used as-is. + */ +@Target([AnnotationTarget.Function]) +annotation PythonCall( + callSpecification: String +) + /** * The qualified name of the corresponding Python module. By default, this is the qualified name of the package. */ diff --git a/tests/resources/generation/expressions/call/input.sdstest b/tests/resources/generation/expressions/call/input.sdstest index 656a10dbe..23ded00e9 100644 --- a/tests/resources/generation/expressions/call/input.sdstest +++ b/tests/resources/generation/expressions/call/input.sdstest @@ -12,9 +12,21 @@ fun h( @PythonName("param_2") param2: Int = 0 ) -> result: Boolean +@PythonCall("$param.i()") +fun i(param: Any?) + +@PythonCall("$param.j($param2)") +fun j(param: Any?, param2: Any?) + +@PythonCall("k($param2, $param)") +fun k(param: Any?, param2: Any?) + pipeline test { f((g(1, 2))); f((g(param2 = 1, param1 = 2))); f((h(1, 2))); f((h(param2 = 1, param1 = 2))); + i("abc"); + j("abc", 123); + k(1.23, 456); } diff --git a/tests/resources/generation/expressions/call/output/tests/generator/call/gen_input.py b/tests/resources/generation/expressions/call/output/tests/generator/call/gen_input.py index e2968155b..1e10a4633 100644 --- a/tests/resources/generation/expressions/call/output/tests/generator/call/gen_input.py +++ b/tests/resources/generation/expressions/call/output/tests/generator/call/gen_input.py @@ -5,3 +5,6 @@ def test(): f(g(2, param2=1)) f(h(1, param_2=2)) f(h(2, param_2=1)) + 'abc'.i() + 'abc'.j(123) + k(456, 1.23) diff --git a/tests/resources/generation/expressions/member access/input.sdstest b/tests/resources/generation/expressions/member access/input.sdstest index 2c8f26592..71a924027 100644 --- a/tests/resources/generation/expressions/member access/input.sdstest +++ b/tests/resources/generation/expressions/member access/input.sdstest @@ -9,6 +9,8 @@ fun h() -> (result1: Boolean, result2: Boolean) class C() { attr a: Int @PythonName("c") attr b: Int + + @PythonCall("$param.i($this)") fun i(param: Any?) } fun factory() -> instance: C? @@ -21,4 +23,5 @@ pipeline test { f(C().b); f(factory()?.a); f(factory()?.b); + f(C().i(1)); } diff --git a/tests/resources/generation/expressions/member access/output/tests/generator/memberAccess/gen_input.py b/tests/resources/generation/expressions/member access/output/tests/generator/memberAccess/gen_input.py index 2d8cbe6ac..b508d00de 100644 --- a/tests/resources/generation/expressions/member access/output/tests/generator/memberAccess/gen_input.py +++ b/tests/resources/generation/expressions/member access/output/tests/generator/memberAccess/gen_input.py @@ -12,3 +12,4 @@ def test(): f(C().c) f(safeds_runner.codegen.safe_access(factory(), 'a')) f(safeds_runner.codegen.safe_access(factory(), 'c')) + f(1.i(C()))