UNPKG

@specs-feup/clava

Version:

A C/C++ source-to-source compiler written in Typescript

701 lines (588 loc) 20.4 kB
import { LaraJoinPoint } from "@specs-feup/lara/api/LaraJoinPoint.js"; import { debug } from "@specs-feup/lara/api/lara/core/LaraCore.js"; import Query from "@specs-feup/lara/api/weaver/Query.js"; import { BinaryOp, Call, DeclStmt, ExprStmt, Expression, FunctionJp, GotoStmt, Joinpoint, LabelDecl, LabelStmt, ParenExpr, ParenType, PointerType, ReturnStmt, Scope, Statement, StorageClass, Type, Vardecl, VariableArrayType, Varref, } from "../../Joinpoints.js"; import ClavaJoinPoints from "../ClavaJoinPoints.js"; export interface InlinerOptions { prefix?: string; } type InlineData = | { type: "assignment"; $target: Expression; $call: Call; } | { type: "call"; $target: undefined; $call: Call; }; export default class Inliner { private options: InlinerOptions; private variableIndex: number = 0; private labelNumber: number = 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: InlinerOptions = { prefix: "__inline" }) { this.options = options; } private getInlinedVarName(originalVarName: string): string { const prefix = this.options["prefix"]; return `${prefix}_${this.variableIndex}_${originalVarName}`; } private hasCycle( $function: FunctionJp, _path: Set<string> = new Set<string>() ): boolean { if (_path.has($function.name)) { return true; } _path.add($function.name); for (const $jp of $function.getDescendants("exprStmt")) { const $exprStmt = $jp as ExprStmt; 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: FunctionJp, _visited: Set<string> = new Set<string>() ) { 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 as ExprStmt; const inlineData = this.checkInline($exprStmt); if (inlineData === undefined) { continue; } const $callee = inlineData.$call.definition; this.inlineFunctionTree($callee, _visited); this.inlinePrivate($exprStmt, inlineData); } return true; } private getInitStmts($varDecl: Vardecl, $expr: Expression): ExprStmt[] { 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 * */ private extractInlineData($exprStmt: ExprStmt): InlineData | undefined { 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: ExprStmt): InlineData | undefined { // 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: ExprStmt): boolean { const inlineData = this.checkInline($exprStmt); if (inlineData === undefined) { return false; } this.inlinePrivate($exprStmt, inlineData); return true; } private inlinePrivate($exprStmt: ExprStmt, inlineData: InlineData): void { 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<string, Vardecl | Expression>(); const paramDeclStmts: Statement[] = []; // 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 as DeclStmt; 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() as Scope; 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 as ReturnStmt; 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") as FunctionJp; const addedDeclarations = new Set<string>(); 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++; } private processBodyToInline( $newNodes: Scope, newVariableMap: Map<string, Vardecl | Expression>, $call: 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. */ private renameLabels(): void { // Maps label names to new LabelDecl const newLabels: Record<string, LabelDecl> = {}; // Visit all gotoStmt and labelStmt for (const jp of Query.search(Joinpoint, { self: ($jp: LaraJoinPoint) => $jp instanceof GotoStmt || $jp instanceof LabelStmt, })) { const $jp = jp as GotoStmt | LabelStmt; // 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: LabelDecl | undefined = 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+_.+$/; private createNewLabelName(previousName: string): string { // 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) ); } private updateVarDecls( $newNodes: Scope, newVariableMap: Map<string, Vardecl | Expression> ): void { // 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 as DeclStmt; 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)); } } } private updateVarrefs( $newNodes: Scope, newVariableMap: Map<string, Vardecl | Expression>, $call: Call ): void { // Update varrefs for (const $jp of $newNodes.getDescendants("varref")) { const $varRef = $jp as Varref; if ($varRef.kind === "function_call") { continue; } const $varDecl = $varRef.decl as Vardecl; // 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() as Vardecl; // Remove initialization $varDeclNoInit.removeInit(false); // Change storage class to extern $varDeclNoInit.storageClass = StorageClass.EXTERN; ($call.getAncestor("function") as FunctionJp).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 as Joinpoint).joinPointType + "'" ); } } } private updateVarrefsInTypes( $newNodes: Scope, newVariableMap: Map<string, Vardecl | Expression>, $call: Call ): void { // 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; } } } private updateType( type: Type, $call: Call, newVariableMap: Map<string, Vardecl | Expression> ): Type { // 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() as PointerType; 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() as ParenType; 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() as Varref; // 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() as VariableArrayType; newType.elementType = updated; newType.sizeExpr = $newVarref; return newType; } } // By default, return type with no changes return type; } private updateVarRef( $varRef: Varref, $call: Call, newVariableMap: Map<string, Vardecl | Expression> ) { const $varDecl = $varRef.decl as Vardecl; // 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() as Vardecl; // Remove initialization $varDeclNoInit.removeInit(false); // Change storage class to extern $varDeclNoInit.storageClass = StorageClass.EXTERN; ($call.getAncestor("function") as FunctionJp).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 as Joinpoint).joinPointType}'` ); } }