Skip to content

Commit

Permalink
fix: generation of dynamic member function calls with memoization + p…
Browse files Browse the repository at this point in the history
…ropagating of impurity information across chained calls (#1015)

Closes #1012
closes #1013

### Summary of Changes

- fix generation of dynamic member function calls with memoization 
- fix propagating of impurity information across chained calls
- added a lot of tests to verify

The new `memoized_dynamic_call` is introduced + `memoized_static_call`
is used for global and static functions.
Calculating global / static function keys directly in the runner would
be a larger refactor and is not implemented here.
As there is no dynamic dispatch / inheritance for global / static
functions, this is not (as) important.

As a drive-by-fix, python calls on class members are now properly
memoized, including the instance the python call was declared on.

The code is also refactored to be a bit simpler, as the
`memoized_dynamic_call` function defers some of the responsibility to
the runner.

---------

Co-authored-by: megalinter-bot <[email protected]>
Co-authored-by: Lars Reimann <[email protected]>
  • Loading branch information
3 people authored Apr 9, 2024
1 parent f888027 commit 19015c3
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 80 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Safely develop Data Science programs with a statically checked domain specific l
2. To _execute_ Safe-DS programs, the [Safe-DS Runner](https://github.com/Safe-DS/Runner) has to be installed and
configured additionally:
1. Install [Python](https://www.python.org/) (3.11 or 3.12).
2. Run `pip install "safe-ds-runner>=0.8.0,<0.9.0"` in a command line to download the latest matching Runner version
2. Run `pip install "safe-ds-runner>=0.9.0,<0.10.0"` in a command line to download the latest matching Runner version
from [PyPI](https://pypi.org/project/safe-ds-runner/).
3. If the Visual Studio Code extension cannot start the runner, adjust the setting `safe-ds.runner.command`.
Enter the absolute path to the Runner executable, as seen in the image below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -959,9 +959,21 @@ export class SafeDsPythonGenerator {
const memoizedArgs = getParameters(callable).map(
(parameter) => this.nodeMapper.callToParameterValue(expression, parameter)!,
);
if (isSdsFunction(callable) && !isStatic(callable) && isSdsMemberAccess(expression.receiver)) {
return expandTracedToNode(
expression,
)`${RUNNER_PACKAGE}.memoized_dynamic_call("${this.getPythonNameOrDefault(
callable,
)}", lambda *_ : ${generatedPythonCall}, [${thisParam}, ${joinTracedToNode(
expression.argumentList,
'arguments',
)(memoizedArgs, (arg) => this.generateExpression(arg, frame), {
separator: ', ',
})}], [${joinToNode(hiddenParameters, (param) => param, { separator: ', ' })}])`;
}
return expandTracedToNode(
expression,
)`${RUNNER_PACKAGE}.memoized_call("${this.generateFullyQualifiedFunctionName(
)`${RUNNER_PACKAGE}.memoized_static_call("${this.generateFullyQualifiedFunctionName(
expression,
)}", lambda *_ : ${generatedPythonCall}, [${joinTracedToNode(expression.argumentList, 'arguments')(
memoizedArgs,
Expand Down Expand Up @@ -1023,26 +1035,36 @@ export class SafeDsPythonGenerator {
Parameter.isOptional(this.nodeMapper.argumentToParameter(arg)),
);
const fullyQualifiedTargetName = this.generateFullyQualifiedFunctionName(expression);
if (isSdsFunction(callable) && !isStatic(callable) && isSdsMemberAccess(expression.receiver)) {
return expandTracedToNode(
expression,
)`${RUNNER_PACKAGE}.memoized_dynamic_call("${this.getPythonNameOrDefault(callable)}", ${
containsOptionalArgs ? 'lambda *_ : ' : ''
}${
containsOptionalArgs ? this.generatePlainCall(expression, sortedArgs, frame) : 'None'
}, [${generateThisParam ? thisParam : ''}${
generateThisParam && memoizedArgs.length > 0 ? ', ' : ''
}${joinTracedToNode(expression.argumentList, 'arguments')(
memoizedArgs,
(arg) => this.generateExpression(arg, frame),
{
separator: ', ',
},
)}], [${joinToNode(hiddenParameters, (param) => param, { separator: ', ' })}])`;
}
if (!containsOptionalArgs && isSdsMemberAccess(expression.receiver)) {
const classDeclaration = getOutermostContainerOfType(callable, isSdsClass)!;
const referenceImport = this.createImportDataForNode(classDeclaration, expression.receiver.member!);
frame.addImport(referenceImport);
}
return expandTracedToNode(expression)`${RUNNER_PACKAGE}.memoized_call("${fullyQualifiedTargetName}", ${
return expandTracedToNode(expression)`${RUNNER_PACKAGE}.memoized_static_call("${fullyQualifiedTargetName}", ${
containsOptionalArgs ? 'lambda *_ : ' : ''
}${
containsOptionalArgs
? this.generatePlainCall(expression, sortedArgs, frame)
: isSdsMemberAccess(expression.receiver) && isSdsCall(expression.receiver.receiver)
? isSdsMemberAccess(expression.receiver.receiver.receiver)
? this.getClassQualifiedNameForMember(<SdsClassMember>callable)
: expandTracedToNode(expression.receiver)`${this.generateExpression(
expression.receiver.receiver.receiver,
frame,
)}.${this.generateExpression(expression.receiver.member!, frame)}`
: isSdsMemberAccess(expression.receiver)
? this.getClassQualifiedNameForMember(<SdsClassMember>callable)
: this.generateExpression(expression.receiver, frame)
: isSdsMemberAccess(expression.receiver)
? this.getClassQualifiedNameForMember(<SdsClassMember>callable)
: this.generateExpression(expression.receiver, frame)
}, [${generateThisParam ? thisParam : ''}${
generateThisParam && memoizedArgs.length > 0 ? ', ' : ''
}${joinTracedToNode(expression.argumentList, 'arguments')(
Expand All @@ -1055,7 +1077,9 @@ export class SafeDsPythonGenerator {
}

private getMemoizedCallHiddenParameters(expression: SdsCall, frame: GenerationInfoFrame): CompositeGeneratorNode[] {
const impurityReasons = this.purityComputer.getImpurityReasonsForExpression(expression);
const impurityReasons = this.purityComputer.getImpurityReasonsForCallable(
this.nodeMapper.callToCallable(expression),
);
const hiddenParameters: CompositeGeneratorNode[] = [];
for (const reason of impurityReasons) {
if (reason instanceof FileRead) {
Expand Down
4 changes: 2 additions & 2 deletions packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { SafeDsMessagingProvider } from '../lsp/safe-ds-messaging-provider.js';

export const RPC_RUNNER_STARTED = 'runner/started';

const LOWEST_SUPPORTED_VERSION = '0.8.0';
const LOWEST_UNSUPPORTED_VERSION = '0.9.0';
const LOWEST_SUPPORTED_VERSION = '0.9.0';
const LOWEST_UNSUPPORTED_VERSION = '0.10.0';
const npmVersionRange = `>=${LOWEST_SUPPORTED_VERSION} <${LOWEST_UNSUPPORTED_VERSION}`;
const pipVersionRange = `>=${LOWEST_SUPPORTED_VERSION},<${LOWEST_UNSUPPORTED_VERSION}`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,37 @@ def segment_a(a):
# Pipelines --------------------------------------------------------------------

def test():
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(1, param2=2), [1, 2], []))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(2, param2=1), [2, 1], []))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(1, param_2=2), [1, 2], []))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(2, param_2=1), [2, 1], []))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", h, [2, 0], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(1, param2=2), [1, 2], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(2, param2=1), [2, 1], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(1, param_2=2), [1, 2], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(2, param_2=1), [2, 1], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", h, [2, 0], []))
'abc'.i()
'abc'.j(123)
k(456, 1.23)
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(1, param2=2), [1, 2], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(2, param2=1), [2, 1], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(1, param_2=2), [1, 2], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(2, param_2=1), [2, 1], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.h", h, [2, 0], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(1, param2=2), [1, 2], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.g", lambda *_ : g(2, param2=1), [2, 1], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(1, param_2=2), [1, 2], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", lambda *_ : h(2, param_2=1), [2, 1], [])))
__gen_null_safe_call(f, lambda: f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.h", h, [2, 0], [])))
__gen_null_safe_call(i, lambda: 'abc'.i())
__gen_null_safe_call(j, lambda: 'abc'.j(123))
__gen_null_safe_call(k, lambda: k(456, 1.23))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.readFile", readFile, [], [safeds_runner.file_mtime('a.txt')]))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.readFile", readFile, [], [safeds_runner.file_mtime('a.txt')]))
f(l(lambda a: segment_a(a)))
f(l(lambda a: (3) * (segment_a(a))))
f(l(lambda a: safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [(3) * (segment_a(a))], [])))
f(l(lambda a: (3) * (safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [(3) * (segment_a(a))], [])], []))))
f(l(lambda a: safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [(3) * (segment_a(a))], [])))
f(l(lambda a: (3) * (safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [(3) * (segment_a(a))], [])], []))))
def __gen_block_lambda_0(a):
__gen_block_lambda_result_result = segment_a(a)
return __gen_block_lambda_result_result
f(l(__gen_block_lambda_0))
def __gen_block_lambda_1(a):
__gen_block_lambda_result_result = safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [segment_a(a)], [])
__gen_block_lambda_result_result = safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [segment_a(a)], [])
return __gen_block_lambda_result_result
f(l(__gen_block_lambda_1))
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.l", l, [lambda a: (3) * (a)], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.l", l, [lambda a: (3) * (a)], []))
def __gen_block_lambda_2(a):
__gen_block_lambda_result_result = (3) * (safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [a], []))
__gen_block_lambda_result_result = (3) * (safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.m", m, [a], []))
return __gen_block_lambda_result_result
f(safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.main.l", l, [__gen_block_lambda_2], []))
f(safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.main.l", l, [__gen_block_lambda_2], []))

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
# Pipelines --------------------------------------------------------------------

def test():
a = safeds_runner.memoized_call("tests.generator.runnerIntegration.expressions.calls.ofClasses.MyClass", MyClass, [0], [])
a = safeds_runner.memoized_static_call("tests.generator.runnerIntegration.expressions.calls.ofClasses.MyClass", MyClass, [0], [])
safeds_runner.save_placeholder('a', a)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 19015c3

Please sign in to comment.