Skip to content

Add a generic utility function for transforming metadata attached to AST nodes #1639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions 0001-feat-fix-ast-map-do-not-map-virtual-nodes.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
From 83b316930ae6dc416fddbb9ef56d5994cdff642a Mon Sep 17 00:00:00 2001
From: Florian Sihler <[email protected]>
Date: Thu, 19 Jun 2025 21:27:22 +0200
Subject: [PATCH 1/2] feat-fix(ast-map): do not map virtual nodes

---
.../datatype-query/datatype-query-executor.ts | 4 +--
.../lang-4.x/ast/model/processing/decorate.ts | 28 ++++---------------
src/typing/infer.ts | 6 ++--
test/functionality/typing/variables.test.ts | 8 +++---
4 files changed, 15 insertions(+), 31 deletions(-)

diff --git a/src/queries/catalog/datatype-query/datatype-query-executor.ts b/src/queries/catalog/datatype-query/datatype-query-executor.ts
index adfbc2c7e..1be109540 100644
--- a/src/queries/catalog/datatype-query/datatype-query-executor.ts
+++ b/src/queries/catalog/datatype-query/datatype-query-executor.ts
@@ -1,7 +1,7 @@
import type { DatatypeQuery, DatatypeQueryResult } from './datatype-query-format';
import { log } from '../../../util/log';
import type { BasicQueryData } from '../../base-query-format';
-import type { NormalizedAst } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate';
+import type { NormalizedAst, ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate';
import { slicingCriterionToId } from '../../../slicing/criterion/parse';
import { inferDataTypes } from '../../../typing/infer';

@@ -13,7 +13,7 @@ export function executeDatatypeQuery({ dataflow, ast }: BasicQueryData, queries:
log.warn('Duplicate criterion in datatype query:', criterion);
continue;
}
- const typedAst = inferDataTypes(ast as NormalizedAst<{ typeVariable?: undefined }>, dataflow);
+ const typedAst = inferDataTypes(ast as NormalizedAst< ParentInformation & { typeVariable?: undefined }>, dataflow);
const node = criterion !== undefined ? typedAst.idMap.get(slicingCriterionToId(criterion, typedAst.idMap)) : typedAst.ast;
if(node === undefined) {
log.warn('Criterion not found in normalized AST:', criterion);
diff --git a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts
index a07671a81..130259cf4 100644
--- a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts
+++ b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts
@@ -518,28 +518,12 @@ export function mapAstInfo<OldInfo, Down, NewInfo>(ast: RNode<OldInfo>, down: Do
});
}

-export function mapNormalizedAstInfo<OldInfo, NewInfo>(normalizedAst: NormalizedAst<OldInfo>, infoMapper: (node: RNode<OldInfo & ParentInformation>) => NewInfo): NormalizedAst<NewInfo> {
- const fullInfoMapper = (node: RNode<OldInfo & ParentInformation>): NewInfo & ParentInformation & Source => {
- const sourceInfo = {
- ...(node.info.fullRange !== undefined ? { fullRange: node.info.fullRange } : {}),
- ...(node.info.fullLexeme !== undefined ? { fullLexeme: node.info.fullLexeme } : {}),
- ...(node.info.additionalTokens !== undefined ? { additionalTokens: node.info.additionalTokens } : {}),
- ...(node.info.file !== undefined ? { file: node.info.file } : {})
- };
- const parentInfo = {
- id: node.info.id,
- parent: node.info.parent,
- role: node.info.role,
- nesting: node.info.nesting,
- index: node.info.index
- };
- const mappedInfo = infoMapper(node);
- return { ...sourceInfo, ...parentInfo, ...mappedInfo };
- };
-
- for(const node of normalizedAst.idMap.values()) {
- (node.info as unknown as NewInfo & ParentInformation & Source) = fullInfoMapper(node);
+export function mapNormalizedAstInfo<OldInfo extends ParentInformation, NewInfo>(normalizedAst: NormalizedAst<OldInfo>, infoMapper: (node: RNode<OldInfo>) => NewInfo): NormalizedAst<NewInfo> {
+ for(const [id, node] of normalizedAst.idMap.entries()) {
+ if(id === node.info.id) { // we skip virtual nodes
+ (node.info as unknown as NewInfo) = infoMapper(node);
+ }
}
-
+
return normalizedAst as unknown as NormalizedAst<NewInfo>;
}
\ No newline at end of file
diff --git a/src/typing/infer.ts b/src/typing/infer.ts
index be111c3ea..8cea5b3a9 100644
--- a/src/typing/infer.ts
+++ b/src/typing/infer.ts
@@ -24,7 +24,7 @@ import { RType } from '../r-bridge/lang-4.x/ast/model/type';
import type { NoInfo } from '../r-bridge/lang-4.x/ast/model/model';
import { RFalse, RTrue } from '../r-bridge/lang-4.x/convert-values';

-export function inferDataTypes<Info extends { typeVariable?: undefined }>(ast: NormalizedAst<Info>, dataflowInfo: DataflowInformation): NormalizedAst<Info & DataTypeInfo> {
+export function inferDataTypes<Info extends ParentInformation & { typeVariable?: undefined }>(ast: NormalizedAst<ParentInformation & Info>, dataflowInfo: DataflowInformation): NormalizedAst<Info & DataTypeInfo> {
const astWithTypeVars = decorateTypeVariables(ast);
const controlFlowInfo = extractCfg(astWithTypeVars, dataflowInfo.graph, ['unique-cf-sets', 'analyze-dead-code', 'remove-dead-code']);
const config = {
@@ -48,11 +48,11 @@ export type DataTypeInfo = {
inferredType: RDataType;
}

-function decorateTypeVariables<OtherInfo>(ast: NormalizedAst<OtherInfo>): NormalizedAst<OtherInfo & UnresolvedTypeInfo> {
+function decorateTypeVariables<Info extends ParentInformation>(ast: NormalizedAst<Info>): NormalizedAst<Info & UnresolvedTypeInfo> {
return mapNormalizedAstInfo(ast, node => ({ ...node.info, typeVariable: new RTypeVariable() }));
}

-function resolveTypeVariables<Info extends UnresolvedTypeInfo>(ast: NormalizedAst<Info>): NormalizedAst<Omit<Info, keyof UnresolvedTypeInfo> & DataTypeInfo> {
+function resolveTypeVariables<Info extends ParentInformation & UnresolvedTypeInfo>(ast: NormalizedAst<Info>): NormalizedAst<Omit<Info, keyof UnresolvedTypeInfo> & DataTypeInfo> {
return mapNormalizedAstInfo(ast, node => {
const { typeVariable, ...rest } = node.info;
return { ...rest, inferredType: resolveType(typeVariable) };
diff --git a/test/functionality/typing/variables.test.ts b/test/functionality/typing/variables.test.ts
index 2c1ccb907..9fbf4982d 100644
--- a/test/functionality/typing/variables.test.ts
+++ b/test/functionality/typing/variables.test.ts
@@ -1,14 +1,14 @@
import { describe } from 'vitest';
-import { RDoubleType, RNullType } from '../../../src/typing/types';
-import { assertInferredType, assertInferredTypes } from '../_helper/typing/assert-inferred-type';
+import { RDoubleType } from '../../../src/typing/types';
+import { assertInferredTypes } from '../_helper/typing/assert-inferred-type';
import { Q } from '../../../src/search/flowr-search-builder';

-describe('Infer types for variables', () => {
+describe.only('Infer types for variables', () => {
assertInferredTypes(
'x <- 42; x',
{ query: Q.var('x').first().build(), expectedType: new RDoubleType() },
{ query: Q.criterion('1@<-').build(), expectedType: new RDoubleType() },
{ query: Q.var('x').last().build(), expectedType: new RDoubleType() }
);
- assertInferredType('y', new RNullType());
+ /* assertInferredType('y', new RNullType()); */
});
\ No newline at end of file
--
2.49.0

2 changes: 1 addition & 1 deletion src/r-bridge/lang-4.x/ast/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type NoInfo = object;
* Will be used to reconstruct the source of the given element in the R-ast.
* This will not be part of most comparisons as it is mainly of interest to the reconstruction of R code.
*/
interface Source {
export interface Source {
/**
* The range is different from the assigned {@link Location} as it refers to the complete source range covered by the given
* element.
Expand Down
66 changes: 65 additions & 1 deletion src/r-bridge/lang-4.x/ast/model/processing/decorate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @module
*/

import type { NoInfo, RNode } from '../model';
import type { NoInfo, RNode, Source } from '../model';
import { guard } from '../../../../../util/assert';
import type { SourceRange } from '../../../../../util/range';
import { BiMap } from '../../../../../util/collections/bimap';
Expand Down Expand Up @@ -463,3 +463,67 @@ function createFoldForFunctionArgument<OtherInfo>(info: FoldInfo<OtherInfo>) {
return decorated;
};
}


export function mapAstInfo<OldInfo, Down, NewInfo>(ast: RNode<OldInfo>, down: Down, infoMapper: (node: RNode<OldInfo>, down: Down) => NewInfo, downUpdater: (node: RNode<OldInfo>, down: Down) => Down = (_node, down) => down): RNode<NewInfo> {
const fullInfoMapper = (node: RNode<OldInfo>, down: Down): NewInfo & Source => {
const sourceInfo = {
...(node.info.fullRange !== undefined ? { fullRange: node.info.fullRange } : {}),
...(node.info.fullLexeme !== undefined ? { fullLexeme: node.info.fullLexeme } : {}),
...(node.info.additionalTokens !== undefined ? { additionalTokens: node.info.additionalTokens } : {}),
...(node.info.file !== undefined ? { file: node.info.file } : {})
};
const mappedInfo = infoMapper(node, down);
return { ...sourceInfo, ...mappedInfo };
};

function updateInfo(n: RNode<OldInfo>, down: Down): RNode<NewInfo> {
(n.info as NewInfo) = fullInfoMapper(n, down);
return n as unknown as RNode<NewInfo>;
}

return foldAstStateful(ast, down, {
down: downUpdater,
foldNumber: updateInfo,
foldString: updateInfo,
foldLogical: updateInfo,
foldSymbol: updateInfo,
foldAccess: (node, _name, _access, down) => updateInfo(node, down),
foldBinaryOp: (op, _lhs, _rhs, down) => updateInfo(op, down),
foldPipe: (op, _lhs, _rhs, down) => updateInfo(op, down),
foldUnaryOp: (op, _operand, down) => updateInfo(op, down),
loop: {
foldFor: (loop, _variable, _vector, _body, down) => updateInfo(loop, down),
foldWhile: (loop, _condition, _body, down) => updateInfo(loop, down),
foldRepeat: (loop, _body, down) => updateInfo(loop, down),
foldNext: (next, down) => updateInfo(next, down),
foldBreak: (next, down) => updateInfo(next, down),
},
other: {
foldComment: (comment, down) => updateInfo(comment, down),
foldLineDirective: (comment, down) => updateInfo(comment, down),
},
foldIfThenElse: (ifThenExpr, _condition, _then, _otherwise, down ) =>
updateInfo(ifThenExpr, down),
foldExprList: (exprList, _grouping, _expressions, down) => updateInfo(exprList, down),
functions: {
foldFunctionDefinition: (definition, _parameters, _body, down) => updateInfo(definition, down),
/** folds named and unnamed function calls */
foldFunctionCall: (call, _functionNameOrExpression, _args, down) => updateInfo(call, down),
/** The `name` is `undefined` if the argument is unnamed, the value, if we have something like `x=,...` */
foldArgument: (argument, _name, _value, down) => updateInfo(argument, down),
/** The `defaultValue` is `undefined` if the argument was not initialized with a default value */
foldParameter: (parameter, _name, _defaultValue, down) => updateInfo(parameter, down)
}
});
}

export function mapNormalizedAstInfo<OldInfo extends ParentInformation, NewInfo>(normalizedAst: NormalizedAst<OldInfo>, infoMapper: (node: RNode<OldInfo>) => NewInfo): NormalizedAst<NewInfo> {
for(const [id, node] of normalizedAst.idMap.entries()) {
if(id === node.info.id) { // we skip virtual nodes
(node.info as unknown as NewInfo) = infoMapper(node);
}
}

return normalizedAst as unknown as NormalizedAst<NewInfo>;
}
Loading