Skip to content

Commit 15114df

Browse files
WinPlay02megalinter-botlars-reimann
authored
feat: PythonCall annotation (#684)
Closes #617 ### Summary of Changes - added `PythonCall` annotation to builtins - added code generation for direct function calls and member access function calls - added tests --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Co-authored-by: Lars Reimann <mail@larsreimann.com>
1 parent f23fa29 commit 15114df

File tree

8 files changed

+109
-44
lines changed

8 files changed

+109
-44
lines changed

src/cli/generator.ts

+61-43
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isSdsEnumVariant,
1919
isSdsExpressionLambda,
2020
isSdsExpressionStatement,
21+
isSdsFunction,
2122
isSdsIndexedAccess,
2223
isSdsInfixOperation,
2324
isSdsList,
@@ -59,12 +60,12 @@ import { NodeFileSystem } from 'langium/node';
5960
import {
6061
getAbstractResults,
6162
getAssignees,
62-
streamBlockLambdaResults,
6363
getImportedDeclarations,
6464
getImports,
65-
isRequiredParameter,
6665
getModuleMembers,
6766
getStatements,
67+
isRequiredParameter,
68+
streamBlockLambdaResults,
6869
} from '../language/helpers/nodeProperties.js';
6970
import { IdManager } from '../language/helpers/idManager.js';
7071
import { isInStubFile } from '../language/helpers/fileExtensions.js';
@@ -209,7 +210,7 @@ const generateParameter = function (
209210
frame: GenerationInfoFrame,
210211
defaultValue: boolean = true,
211212
): string {
212-
return expandToString`${getPythonNameOrDefault(frame.getServices(), parameter)}${
213+
return expandToString`${getPythonNameOrDefault(frame.services, parameter)}${
213214
defaultValue && parameter.defaultValue !== undefined
214215
? '=' + generateExpression(parameter.defaultValue, frame)
215216
: ''
@@ -291,7 +292,7 @@ const generateStatement = function (statement: SdsStatement, frame: GenerationIn
291292

292293
const generateAssignment = function (assignment: SdsAssignment, frame: GenerationInfoFrame): string {
293294
const requiredAssignees = isSdsCall(assignment.expression)
294-
? getAbstractResults(frame.getServices().helpers.NodeMapper.callToCallable(assignment.expression)).length
295+
? getAbstractResults(frame.services.helpers.NodeMapper.callToCallable(assignment.expression)).length
295296
: /* c8 ignore next */
296297
1;
297298
const assignees = getAssignees(assignment);
@@ -347,7 +348,7 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio
347348
}
348349
}
349350

350-
const partiallyEvaluatedNode = frame.getServices().evaluation.PartialEvaluator.evaluate(expression);
351+
const partiallyEvaluatedNode = frame.services.evaluation.PartialEvaluator.evaluate(expression);
351352
if (partiallyEvaluatedNode instanceof BooleanConstant) {
352353
return partiallyEvaluatedNode.value ? 'True' : 'False';
353354
} else if (partiallyEvaluatedNode instanceof IntConstant) {
@@ -360,39 +361,44 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio
360361
} else if (partiallyEvaluatedNode instanceof StringConstant) {
361362
return `'${formatStringSingleLine(partiallyEvaluatedNode.value)}'`;
362363
}
363-
// Handled after constant expressions: EnumVariant, List, Map
364364

365-
if (isSdsTemplateString(expression)) {
365+
// Handled after constant expressions: EnumVariant, List, Map
366+
else if (isSdsTemplateString(expression)) {
366367
return `f'${expression.expressions.map((expr) => generateExpression(expr, frame)).join('')}'`;
367-
}
368-
369-
if (isSdsMap(expression)) {
368+
} else if (isSdsMap(expression)) {
370369
const mapContent = expression.entries.map(
371370
(entry) => `${generateExpression(entry.key, frame)}: ${generateExpression(entry.value, frame)}`,
372371
);
373372
return `{${mapContent.join(', ')}}`;
374-
}
375-
if (isSdsList(expression)) {
373+
} else if (isSdsList(expression)) {
376374
const listContent = expression.elements.map((value) => generateExpression(value, frame));
377375
return `[${listContent.join(', ')}]`;
378-
}
379-
380-
if (isSdsBlockLambda(expression)) {
376+
} else if (isSdsBlockLambda(expression)) {
381377
return frame.getUniqueLambdaBlockName(expression);
382-
}
383-
if (isSdsCall(expression)) {
384-
const sortedArgs = sortArguments(frame.getServices(), expression.argumentList.arguments);
378+
} else if (isSdsCall(expression)) {
379+
const callable = frame.services.helpers.NodeMapper.callToCallable(expression);
380+
if (isSdsFunction(callable)) {
381+
const pythonCall = frame.services.builtins.Annotations.getPythonCall(callable);
382+
if (pythonCall) {
383+
let thisParam: string | undefined = undefined;
384+
if (isSdsMemberAccess(expression.receiver)) {
385+
thisParam = generateExpression(expression.receiver.receiver, frame);
386+
}
387+
const argumentsMap = getArgumentsMap(expression.argumentList.arguments, frame);
388+
return generatePythonCall(pythonCall, argumentsMap, thisParam);
389+
}
390+
}
391+
392+
const sortedArgs = sortArguments(frame.services, expression.argumentList.arguments);
385393
return expandToString`${generateExpression(expression.receiver, frame)}(${sortedArgs
386394
.map((arg) => generateArgument(arg, frame))
387395
.join(', ')})`;
388-
}
389-
if (isSdsExpressionLambda(expression)) {
396+
} else if (isSdsExpressionLambda(expression)) {
390397
return `lambda ${generateParameters(expression.parameterList, frame)}: ${generateExpression(
391398
expression.result,
392399
frame,
393400
)}`;
394-
}
395-
if (isSdsInfixOperation(expression)) {
401+
} else if (isSdsInfixOperation(expression)) {
396402
const leftOperand = generateExpression(expression.leftOperand, frame);
397403
const rightOperand = generateExpression(expression.rightOperand, frame);
398404
switch (expression.operator) {
@@ -412,14 +418,12 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio
412418
default:
413419
return `(${leftOperand}) ${expression.operator} (${rightOperand})`;
414420
}
415-
}
416-
if (isSdsIndexedAccess(expression)) {
421+
} else if (isSdsIndexedAccess(expression)) {
417422
return expandToString`${generateExpression(expression.receiver, frame)}[${generateExpression(
418423
expression.index,
419424
frame,
420425
)}]`;
421-
}
422-
if (isSdsMemberAccess(expression)) {
426+
} else if (isSdsMemberAccess(expression)) {
423427
const member = expression.member?.target.ref!;
424428
const receiver = generateExpression(expression.receiver, frame);
425429
if (isSdsEnumVariant(member)) {
@@ -442,31 +446,49 @@ const generateExpression = function (expression: SdsExpression, frame: Generatio
442446
return `${receiver}.${memberExpression}`;
443447
}
444448
}
445-
}
446-
if (isSdsParenthesizedExpression(expression)) {
449+
} else if (isSdsParenthesizedExpression(expression)) {
447450
return expandToString`${generateExpression(expression.expression, frame)}`;
448-
}
449-
if (isSdsPrefixOperation(expression)) {
451+
} else if (isSdsPrefixOperation(expression)) {
450452
const operand = generateExpression(expression.operand, frame);
451453
switch (expression.operator) {
452454
case 'not':
453455
return expandToString`not (${operand})`;
454456
case '-':
455457
return expandToString`-(${operand})`;
456458
}
457-
}
458-
if (isSdsReference(expression)) {
459+
} else if (isSdsReference(expression)) {
459460
const declaration = expression.target.ref!;
460461
const referenceImport =
461-
getExternalReferenceNeededImport(frame.getServices(), expression, declaration) ||
462-
getInternalReferenceNeededImport(frame.getServices(), expression, declaration);
462+
getExternalReferenceNeededImport(frame.services, expression, declaration) ||
463+
getInternalReferenceNeededImport(frame.services, expression, declaration);
463464
frame.addImport(referenceImport);
464-
return referenceImport?.alias || getPythonNameOrDefault(frame.getServices(), declaration);
465+
return referenceImport?.alias || getPythonNameOrDefault(frame.services, declaration);
465466
}
466467
/* c8 ignore next 2 */
467468
throw new Error(`Unknown expression type: ${expression.$type}`);
468469
};
469470

471+
const generatePythonCall = function (
472+
pythonCall: string,
473+
argumentsMap: Map<string, string>,
474+
thisParam: string | undefined = undefined,
475+
): string {
476+
if (thisParam) {
477+
argumentsMap.set('this', thisParam);
478+
}
479+
480+
return pythonCall.replace(/\$[_a-zA-Z][_a-zA-Z0-9]*/gu, (value) => argumentsMap.get(value.substring(1))!);
481+
};
482+
483+
const getArgumentsMap = function (argumentList: SdsArgument[], frame: GenerationInfoFrame): Map<string, string> {
484+
const argumentsMap = new Map<string, string>();
485+
argumentList.reduce((map, value) => {
486+
map.set(frame.services.helpers.NodeMapper.argumentToParameter(value)?.name!, generateArgument(value, frame));
487+
return map;
488+
}, argumentsMap);
489+
return argumentsMap;
490+
};
491+
470492
const sortArguments = function (services: SafeDsServices, argumentList: SdsArgument[]): SdsArgument[] {
471493
// $containerIndex contains the index of the parameter in the receivers parameter list
472494
const parameters = argumentList.map((argument) => {
@@ -482,7 +504,7 @@ const sortArguments = function (services: SafeDsServices, argumentList: SdsArgum
482504
};
483505

484506
const generateArgument = function (argument: SdsArgument, frame: GenerationInfoFrame) {
485-
const parameter = frame.getServices().helpers.NodeMapper.argumentToParameter(argument);
507+
const parameter = frame.services.helpers.NodeMapper.argumentToParameter(argument);
486508
return expandToString`${
487509
parameter !== undefined && !isRequiredParameter(parameter)
488510
? generateParameter(parameter, frame, false) + '='
@@ -567,9 +589,9 @@ interface ImportData {
567589
}
568590

569591
class GenerationInfoFrame {
570-
services: SafeDsServices;
571-
blockLambdaManager: IdManager<SdsBlockLambda>;
572-
importSet: Map<String, ImportData>;
592+
readonly services: SafeDsServices;
593+
private readonly blockLambdaManager: IdManager<SdsBlockLambda>;
594+
private readonly importSet: Map<String, ImportData>;
573595

574596
constructor(services: SafeDsServices, importSet: Map<String, ImportData> = new Map<String, ImportData>()) {
575597
this.services = services;
@@ -589,10 +611,6 @@ class GenerationInfoFrame {
589611
getUniqueLambdaBlockName(lambda: SdsBlockLambda): string {
590612
return `${BLOCK_LAMBDA_PREFIX}${this.blockLambdaManager.assignId(lambda)}`;
591613
}
592-
593-
getServices(): SafeDsServices {
594-
return this.services;
595-
}
596614
}
597615

598616
export interface GenerateOptions {

src/language/builtins/safe-ds-annotations.ts

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
SdsAnnotatedObject,
55
SdsAnnotation,
66
SdsEnumVariant,
7+
SdsFunction,
78
SdsModule,
89
SdsParameter,
910
} from '../generated/ast.js';
@@ -65,6 +66,19 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
6566
return this.getAnnotation(IDE_INTEGRATION_URI, 'Expert');
6667
}
6768

69+
getPythonCall(node: SdsFunction | undefined): string | undefined {
70+
const value = this.getArgumentValue(node, this.PythonCall, 'callSpecification');
71+
if (value instanceof StringConstant) {
72+
return value.value;
73+
} else {
74+
return undefined;
75+
}
76+
}
77+
78+
get PythonCall(): SdsAnnotation | undefined {
79+
return this.getAnnotation(CODE_GENERATION_URI, 'PythonCall');
80+
}
81+
6882
getPythonModule(node: SdsModule | undefined): string | undefined {
6983
const value = this.getArgumentValue(node, this.PythonModule, 'qualifiedName');
7084
if (value instanceof StringConstant) {

src/language/validation/names.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export const moduleMemberMustHaveNameThatIsUniqueInPackage = (services: SafeDsSe
235235
let declarationsInPackage: AstNodeDescription[];
236236
let kind: string;
237237
if (packageName.startsWith(BUILTINS_ROOT_PACKAGE)) {
238-
// For a builtin package the simple names of declarations must be unique
238+
// For a builtin package, the simple names of declarations must be unique
239239
declarationsInPackage = packageManager.getDeclarationsInPackageOrSubpackage(BUILTINS_ROOT_PACKAGE);
240240
kind = 'builtin declarations';
241241
} else {

src/resources/builtins/safeds/lang/codeGeneration.sdsstub

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
package safeds.lang
22

3+
/**
4+
* The specification of a corresponding function call in Python. By default, the function is called as specified in the
5+
* stub.
6+
*
7+
* @param callSpecification
8+
* The specification of corresponding Python call. The specification can contain template expression, which are
9+
* replaced by the corresponding arguments of the function call. `$this` is replaced by the receiver of the call.
10+
* `$param` is replaced by the value of the parameter called `param`. Otherwise, the string is used as-is.
11+
*/
12+
@Target([AnnotationTarget.Function])
13+
annotation PythonCall(
14+
callSpecification: String
15+
)
16+
317
/**
418
* The qualified name of the corresponding Python module. By default, this is the qualified name of the package.
519
*/

tests/resources/generation/expressions/call/input.sdstest

+12
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,21 @@ fun h(
1212
@PythonName("param_2") param2: Int = 0
1313
) -> result: Boolean
1414

15+
@PythonCall("$param.i()")
16+
fun i(param: Any?)
17+
18+
@PythonCall("$param.j($param2)")
19+
fun j(param: Any?, param2: Any?)
20+
21+
@PythonCall("k($param2, $param)")
22+
fun k(param: Any?, param2: Any?)
23+
1524
pipeline test {
1625
f((g(1, 2)));
1726
f((g(param2 = 1, param1 = 2)));
1827
f((h(1, 2)));
1928
f((h(param2 = 1, param1 = 2)));
29+
i("abc");
30+
j("abc", 123);
31+
k(1.23, 456);
2032
}

tests/resources/generation/expressions/call/output/tests/generator/call/gen_input.py

+3
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ def test():
55
f(g(2, param2=1))
66
f(h(1, param_2=2))
77
f(h(2, param_2=1))
8+
'abc'.i()
9+
'abc'.j(123)
10+
k(456, 1.23)

tests/resources/generation/expressions/member access/input.sdstest

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ fun h() -> (result1: Boolean, result2: Boolean)
99
class C() {
1010
attr a: Int
1111
@PythonName("c") attr b: Int
12+
13+
@PythonCall("$param.i($this)") fun i(param: Any?)
1214
}
1315

1416
fun factory() -> instance: C?
@@ -21,4 +23,5 @@ pipeline test {
2123
f(C().b);
2224
f(factory()?.a);
2325
f(factory()?.b);
26+
f(C().i(1));
2427
}

tests/resources/generation/expressions/member access/output/tests/generator/memberAccess/gen_input.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ def test():
1212
f(C().c)
1313
f(safeds_runner.codegen.safe_access(factory(), 'a'))
1414
f(safeds_runner.codegen.safe_access(factory(), 'c'))
15+
f(1.i(C()))

0 commit comments

Comments
 (0)