@specs-feup/clava
Version:
A C/C++ source-to-source compiler written in Typescript
481 lines • 20.8 kB
JavaScript
import { debug } from "@specs-feup/lara/api/lara/core/LaraCore.js";
import Query from "@specs-feup/lara/api/weaver/Query.js";
import { BinaryOp, Call, Expression, GotoStmt, Joinpoint, LabelDecl, LabelStmt, ParenExpr, ParenType, PointerType, StorageClass, Vardecl, VariableArrayType, Varref, } from "../../Joinpoints.js";
import ClavaJoinPoints from "../ClavaJoinPoints.js";
export default class Inliner {
options;
variableIndex = 0;
labelNumber = 0;
/**
*
* @param options - Object with options. Supported options: 'prefix' (default: "__inline"), the prefix that will be used in the name of variables inserted by the Inliner
*/
constructor(options = { prefix: "__inline" }) {
this.options = options;
}
getInlinedVarName(originalVarName) {
const prefix = this.options["prefix"];
return `${prefix}_${this.variableIndex}_${originalVarName}`;
}
hasCycle($function, _path = new Set()) {
if (_path.has($function.name)) {
return true;
}
_path.add($function.name);
for (const $jp of $function.getDescendants("exprStmt")) {
const $exprStmt = $jp;
const $expr = $exprStmt.expr;
if (!($expr instanceof BinaryOp &&
$expr.isAssignment &&
$expr.right instanceof Call) &&
!($expr instanceof Call)) {
continue;
}
const $call = $expr instanceof Call ? $expr : $expr.right;
if (!($call instanceof Call) ||
($call instanceof Call && $call.definition == undefined)) {
continue;
}
if (this.hasCycle($call.definition, _path)) {
return true;
}
}
_path.delete($function.name);
return false;
}
inlineFunctionTree($function, _visited = new Set()) {
if ($function === undefined) {
return false;
}
debug("InlineFunctionTree called on " + $function.signature);
if (this.hasCycle($function)) {
return false;
}
if (_visited.has($function.name)) {
return true;
}
_visited.add($function.name);
for (const $jp of $function.getDescendants("exprStmt")) {
const $exprStmt = $jp;
const inlineData = this.checkInline($exprStmt);
if (inlineData === undefined) {
continue;
}
const $callee = inlineData.$call.definition;
this.inlineFunctionTree($callee, _visited);
this.inlinePrivate($exprStmt, inlineData);
}
return true;
}
getInitStmts($varDecl, $expr) {
const $assign = ClavaJoinPoints.assign(ClavaJoinPoints.varRef($varDecl), $expr);
return [ClavaJoinPoints.exprStmt($assign)];
}
/**
*
* @param $exprStmt -
* @returns An object with the properties below or undefined if this exprStmt cannot be inlined.
* Only exprStmt that are an isolated call, or that are an assignment with a single call
* in the right-hand side can be inlined.
*
* - type: a string with either the value 'call' or 'assign', indicating the type of inlining
* that can be applied to the given exprStmt.
* - $target: if the type is 'assign', contains the left-hand side of the assignment. Otherwise, is undefined.
* - $call: the call to be inlined
*
*/
extractInlineData($exprStmt) {
if ($exprStmt.expr instanceof BinaryOp &&
$exprStmt.expr.isAssignment &&
$exprStmt.expr.right instanceof Call) {
return {
type: "assignment",
$target: $exprStmt.expr.left,
$call: $exprStmt.expr.right,
};
}
if ($exprStmt.expr instanceof Call) {
return {
type: "call",
$target: undefined,
$call: $exprStmt.expr,
};
}
return undefined;
}
/**
* Check if the given $exprStmt can be inlined or not. If it can, returns an object with information important for inlining,
* otherwise returns undefined.
*
* A call can be inline if the following rules apply:
* - The exprStmt is an isolated call, or an assignment with a single call in the right-hand side.
* - The call has a definition/implementation available.
* - The call is not a function that is part of the system headers.
*
* @param $exprStmt -
* @returns An object with the properties below or undefined if this exprStmt cannot be inlined.
*
* - type: a string with either the value 'call' or 'assign', indicating the type of inlining
* that can be applied to the given exprStmt.
* - $target: if the type is 'assign', contains the left-hand side of the assignment. Otherwise, is undefined.
* - $call: the call to be inlined
*
*/
checkInline($exprStmt) {
// Extract inline information
const inlineData = this.extractInlineData($exprStmt);
if (inlineData === undefined) {
return undefined;
}
// Check if call has an implementation
if (!inlineData.$call.function.isImplementation) {
debug(`Inliner: call '${inlineData.$call.toString()}' not inlined because implementation was not found`);
return undefined;
}
// Ignore functions that are part of the system headers
if (inlineData.$call.function.isInSystemHeader) {
debug(`Inliner: call '${inlineData.$call.toString()}' not inlined function belongs to a system header`);
return undefined;
}
return inlineData;
}
inline($exprStmt) {
const inlineData = this.checkInline($exprStmt);
if (inlineData === undefined) {
return false;
}
this.inlinePrivate($exprStmt, inlineData);
return true;
}
inlinePrivate($exprStmt, inlineData) {
debug("InlinePrivate called on " + $exprStmt.code + "@" + $exprStmt.location);
const $target = inlineData.$target;
const $call = inlineData.$call;
let args = $call.args;
if (!Array.isArray(args)) {
args = [args];
}
const $function = $call.function;
if ($function.getDescendants("returnStmt").length > 1) {
throw new Error(`'${$function.name}' cannot be inlined: more than one return statement`);
}
const params = $function.params;
const newVariableMap = new Map();
const paramDeclStmts = [];
// TODO: args can be greater than params, if varargs. How to deal with this?
for (let i = 0; i < params.length; i++) {
const $arg = args[i];
const $param = params[i];
// Arrays cannot be assigned
// If param is array or pointer, there is no need to add declaration,
// simply rename the param to the name of the arg
//if ($param.type.isArray || $param.type.isPointer) {
if ($param.type.isArray) {
newVariableMap.set($param.name, $arg);
}
else {
const newName = this.getInlinedVarName($param.name);
const $varDecl = ClavaJoinPoints.varDeclNoInit(newName, $param.type);
const $varDeclStmt = ClavaJoinPoints.declStmt($varDecl);
const $initStmts = this.getInitStmts($varDecl, $arg);
newVariableMap.set($param.name, $varDecl);
paramDeclStmts.push($varDeclStmt, ...$initStmts);
}
}
for (const jp of $function.body.getDescendants("declStmt")) {
const stmt = jp;
const $varDecl = stmt.decls[0];
if (!($varDecl instanceof Vardecl)) {
continue;
}
const newName = this.getInlinedVarName($varDecl.name);
const $newDecl = ClavaJoinPoints.varDeclNoInit(newName, $varDecl.type);
newVariableMap.set($varDecl.name, $newDecl);
}
const $newNodes = $function.body.copy();
this.processBodyToInline($newNodes, newVariableMap, $call);
// Remove/replace return statements
if ($exprStmt.expr instanceof BinaryOp && $exprStmt.expr.isAssignment) {
for (const $jp of $newNodes.getDescendants("returnStmt")) {
const $returnStmt = $jp;
if ($target === undefined) {
throw new Error("Expected $target to be defined when exprStmt is an assignment");
}
else if ($returnStmt.returnExpr !== null &&
$returnStmt.returnExpr !== undefined) {
$returnStmt.replaceWith(ClavaJoinPoints.exprStmt(ClavaJoinPoints.assign($target, $returnStmt.returnExpr)));
}
else {
$returnStmt.detach();
}
}
}
else if ($exprStmt.expr instanceof Call) {
for (const $returnStmt of $newNodes.getDescendants("returnStmt")) {
// Replace the return with a nop (i.e. empty statement), in case there is a label before. Otherwise, just remove return
const left = $returnStmt.siblingsLeft;
if (left.length > 0 && left[left.length - 1] instanceof LabelStmt) {
$returnStmt.replaceWith(ClavaJoinPoints.emptyStmt());
}
else {
$returnStmt.detach();
}
}
}
// For any calls inside $newNodes, add forward declarations before the function, if they have definition
// TODO: this should be done for calls of functions that are on this file. For other files, the corresponding include
// should be added
const $parentFunction = $call.getAncestor("function");
const addedDeclarations = new Set();
for (const $newCall of Query.searchFrom($newNodes, Call)) {
// Ignore functions that are part of the system headers
if ($newCall.function.isInSystemHeader) {
continue;
}
if (addedDeclarations.has($newCall.function.id)) {
continue;
}
addedDeclarations.add($newCall.function.id);
const $newFunctionDecl = ClavaJoinPoints.functionDeclFromType($newCall.function.name, $newCall.function.functionType);
$parentFunction.insertBefore($newFunctionDecl);
}
// Let the function body be on its own scope
// If the function uses local labels they must appear at the beginning of the scope
const inlinedScope = paramDeclStmts.length === 0
? $newNodes
: ClavaJoinPoints.scope(...paramDeclStmts, $newNodes);
$exprStmt.replaceWith(inlinedScope);
this.variableIndex++;
}
processBodyToInline($newNodes, newVariableMap, $call) {
this.updateVarDecls($newNodes, newVariableMap);
this.updateVarrefs($newNodes, newVariableMap, $call);
this.updateVarrefsInTypes($newNodes, newVariableMap, $call);
this.renameLabels();
}
/**
* Labels need to be renamed, to avoid duplicated labels.
*/
renameLabels() {
// Maps label names to new LabelDecl
const newLabels = {};
// Visit all gotoStmt and labelStmt
for (const jp of Query.search(Joinpoint, {
self: ($jp) => $jp instanceof GotoStmt || $jp instanceof LabelStmt,
})) {
const $jp = jp;
// Get original label
const $labelDecl = $jp instanceof GotoStmt ? $jp.label : $jp.decl;
// Get new label, or create if it does not exist yet
let $newLabelDecl = newLabels[$labelDecl.name];
if ($newLabelDecl === undefined) {
const newLabelName = this.createNewLabelName($labelDecl.name);
$newLabelDecl = ClavaJoinPoints.labelDecl(newLabelName);
newLabels[$labelDecl.name] = $newLabelDecl;
}
// Replace label
if ($jp instanceof GotoStmt) {
$jp.label = $newLabelDecl;
}
else {
$jp.decl = $newLabelDecl;
}
}
// If there are any label decls, rename them
for (const $labelDecl of Query.search(LabelDecl)) {
const $newLabelDecl = newLabels[$labelDecl.name];
$labelDecl.replaceWith($newLabelDecl);
}
}
static LABEL_PREFIX_REGEX = /^inliner_\d+_.+$/;
createNewLabelName(previousName) {
// Check if has inliner prefix
if (!Inliner.LABEL_PREFIX_REGEX.test(previousName)) {
const labelNumber = this.labelNumber;
this.labelNumber += 1;
return "inliner_" + labelNumber + "_" + previousName;
}
// Label has already been generated by this function, update number
const newName = previousName.substring("inliner_".length);
// Get number
const underscoreIndex = newName.indexOf("_");
const labelNumber = this.labelNumber;
this.labelNumber += 1;
// Generate new label
return ("inliner_" + labelNumber + "_" + newName.substring(underscoreIndex + 1));
}
updateVarDecls($newNodes, newVariableMap) {
// Replace decl stmts of old vardecls with vardecls of new names (params are not included)
for (const $jp of $newNodes.getDescendants("declStmt")) {
const $declStmt = $jp;
const decls = $declStmt.decls;
for (const $varDecl of decls) {
if (!($varDecl instanceof Vardecl)) {
continue;
}
const newVar = newVariableMap.get($varDecl.name);
// If not found, just continue
if (newVar === undefined) {
debug(`Could not find variable ${$varDecl.name} in variable map`);
continue;
}
else if (!(newVar instanceof Vardecl)) {
throw new Error(`Expected newVar to be a Vardecl, but it is a ${newVar.joinPointType}`);
}
// Replace decl
$declStmt.replaceWith(ClavaJoinPoints.declStmt(newVar));
}
}
}
updateVarrefs($newNodes, newVariableMap, $call) {
// Update varrefs
for (const $jp of $newNodes.getDescendants("varref")) {
const $varRef = $jp;
if ($varRef.kind === "function_call") {
continue;
}
const $varDecl = $varRef.decl;
// If global variable, will not be in the variable map
if ($varDecl !== undefined && $varDecl.isGlobal) {
// Copy vardecl to work over it
const $varDeclNoInit = $varDecl.copy();
// Remove initialization
$varDeclNoInit.removeInit(false);
// Change storage class to extern
$varDeclNoInit.storageClass = StorageClass.EXTERN;
$call.getAncestor("function").insertBefore($varDeclNoInit);
continue;
}
// Verify if there is a mapping
const newVar = newVariableMap.get($varDecl.name);
if (newVar === undefined) {
throw new Error("Could not find variable " +
$varDecl.name +
"@" +
$varRef.location +
" in variable map");
}
// If vardecl, map contains reference to old vardecl, create a varref from the new vardecl
if (newVar instanceof Vardecl) {
$varRef.replaceWith(ClavaJoinPoints.varRef(newVar));
}
// If expression, simply replace varref with the expression
else if (newVar instanceof Expression) {
const $adaptedVar =
// If varref, does not need parenthesis
newVar instanceof Varref
? newVar
: // For other expressions, if parent is already a parenthesis, does not need to add a new one
$varRef.parent instanceof ParenExpr
? newVar
: // Add parenthesis
ClavaJoinPoints.parenthesis(newVar);
$varRef.replaceWith($adaptedVar);
}
else {
throw new Error("Not defined when newVar is of type '" +
newVar.joinPointType +
"'");
}
}
}
updateVarrefsInTypes($newNodes, newVariableMap, $call) {
// Update varrefs inside types
for (const $jp of $newNodes.descendants) {
// If no type, ignore
if (!$jp.hasType) {
continue;
}
const type = $jp.type;
const updatedType = this.updateType(type, $call, newVariableMap);
if (updatedType !== type) {
$jp.type = updatedType;
}
}
}
updateType(type, $call, newVariableMap) {
// Since any type node can be shared, any change must be made in copies
// If pointer type, check pointee
if (type instanceof PointerType) {
const original = type.pointee;
const updated = this.updateType(original, $call, newVariableMap);
if (original !== updated) {
const newType = type.copy();
newType.pointee = updated;
return newType;
}
}
if (type instanceof ParenType) {
const original = type.innerType;
const updated = this.updateType(original, $call, newVariableMap);
if (original !== updated) {
const newType = type.copy();
newType.innerType = updated;
return newType;
}
}
if (type instanceof VariableArrayType) {
// Has to track changes both for element type and its own array expression
// Either was, has to update this type
let hasChanges = false;
// Element type
const original = type.elementType;
const updated = this.updateType(original, $call, newVariableMap);
if (original !== updated) {
hasChanges = true;
}
// TODO: I have no idea if this type cast is correct.
const $sizeExprCopy = type.sizeExpr.copy();
// Update any children of sizeExpr
for (const $varRef of Query.searchFrom($sizeExprCopy, Varref)) {
const $newVarref = this.updateVarRef($varRef, $call, newVariableMap);
if ($newVarref !== $varRef) {
hasChanges = true;
$varRef.replaceWith($newVarref);
}
}
// Update top expr, if needed
const $newVarref = this.updateVarRef($sizeExprCopy, $call, newVariableMap);
if ($newVarref !== $sizeExprCopy) {
hasChanges = true;
}
if (hasChanges) {
const newType = type.copy();
newType.elementType = updated;
newType.sizeExpr = $newVarref;
return newType;
}
}
// By default, return type with no changes
return type;
}
updateVarRef($varRef, $call, newVariableMap) {
const $varDecl = $varRef.decl;
// If global variable, will not be in the variable map
if ($varDecl !== undefined && $varDecl.isGlobal) {
// Copy vardecl to work over it
const $varDeclNoInit = $varDecl.copy();
// Remove initialization
$varDeclNoInit.removeInit(false);
// Change storage class to extern
$varDeclNoInit.storageClass = StorageClass.EXTERN;
$call.getAncestor("function").insertBefore($varDeclNoInit);
return $varRef;
}
const newVar = newVariableMap.get($varDecl.name);
// If not found, just return
if (newVar === undefined) {
return $varRef;
}
// If vardecl, create a new varref
if (newVar instanceof Vardecl) {
return ClavaJoinPoints.varRef(newVar);
}
// If expression, return expression
if (newVar instanceof Expression) {
return newVar;
}
throw new Error(`Case not supported, newVar of type '${newVar.joinPointType}'`);
}
}
//# sourceMappingURL=Inliner.js.map