-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cmp): Introduce basic alpha renaming for top-level struct/messages
- Loading branch information
Showing
17 changed files
with
769 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { | ||
AstConstantDef, | ||
AstReceiverKind, | ||
AstModuleItem, | ||
AstStructFieldInitializer, | ||
AstFunctionAttribute, | ||
AstOpBinary, | ||
AstOpUnary, | ||
AstFieldAccess, | ||
AstConditional, | ||
AstMethodCall, | ||
AstStaticCall, | ||
AstNumber, | ||
AstBoolean, | ||
AstString, | ||
AstStructInstance, | ||
AstInitOf, | ||
AstConstantAttribute, | ||
AstContractAttribute, | ||
AstTypedParameter, | ||
AstImport, | ||
AstNativeFunctionDecl, | ||
AstReceiver, | ||
AstStatementRepeat, | ||
AstStatementUntil, | ||
AstStatementWhile, | ||
AstStatementForEach, | ||
AstStatementTry, | ||
AstStatementTryCatch, | ||
AstCondition, | ||
AstStatementAugmentedAssign, | ||
AstStatementAssign, | ||
AstStatementExpression, | ||
AstStatementReturn, | ||
AstStatementLet, | ||
AstFunctionDef, | ||
AstContract, | ||
AstTrait, | ||
AstId, | ||
AstModule, | ||
AstStructDecl, | ||
AstMessageDecl, | ||
AstFunctionDecl, | ||
AstConstantDecl, | ||
AstContractInit, | ||
AstPrimitiveTypeDecl, | ||
AstTypeId, | ||
AstMapType, | ||
AstBouncedMessageType, | ||
AstFieldDecl, | ||
AstOptionalType, | ||
AstNode, | ||
AstFuncId, | ||
} from "./ast"; | ||
import { createHash } from "crypto"; | ||
import { throwInternalCompilerError } from "../errors"; | ||
|
||
export type AstHash = string; | ||
|
||
/** | ||
* Provides functionality to hash AST nodes regardless of names of the elements. | ||
*/ | ||
export class AstHasher { | ||
private constructor(private readonly sort: boolean) {} | ||
public static make(params: Partial<{ sort: boolean }> = {}): AstHasher { | ||
const { sort = true } = params; | ||
return new AstHasher(sort); | ||
} | ||
|
||
public hash(node: AstNode): AstHash { | ||
const hasher = AstHasher.make(); | ||
switch (node.kind) { | ||
case "struct_decl": | ||
return hasher.hashStructDecl(node); | ||
case "message_decl": | ||
return hasher.hashMessageDecl(node); | ||
default: | ||
throwInternalCompilerError(`Unsupported node: ${node.kind}`); | ||
} | ||
} | ||
|
||
public hashStructDecl(node: AstStructDecl): string { | ||
const fieldsHash = this.hashFields(node.fields); | ||
return this.hashString(`struct|${fieldsHash}`); | ||
} | ||
|
||
public hashMessageDecl(node: AstMessageDecl): string { | ||
const fieldsHash = this.hashFields(node.fields); | ||
return this.hashString(`message|${fieldsHash}|${node.opcode}`); | ||
} | ||
|
||
private hashFields(fields: AstFieldDecl[]): string { | ||
let hashedFields = fields.map((field) => this.hashFieldDecl(field)); | ||
if (this.sort) { | ||
hashedFields = hashedFields.sort(); | ||
} | ||
return hashedFields.join("|"); | ||
} | ||
|
||
private hashString(data: string): string { | ||
return createHash("sha256").update(data).digest("hex"); | ||
} | ||
|
||
public hashFieldDecl(field: AstFieldDecl): string { | ||
const typeHash = this.hashAstNode(field.type); | ||
return `field|${typeHash}`; | ||
} | ||
|
||
private hashAstNode(node: AstNode): string { | ||
switch (node.kind) { | ||
case "type_id": | ||
return node.text; | ||
default: | ||
return node.kind; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import fs from "fs"; | ||
import { __DANGER_resetNodeId } from "../grammar/ast"; | ||
import { parse } from "../grammar/grammar"; | ||
import { join } from "path"; | ||
import { AstRenamer } from "./rename"; | ||
import { prettyPrint } from "../prettyPrinter"; | ||
import * as assert from "assert"; | ||
|
||
const TEST_DIR = join(__dirname, "..", "test", "contracts"); | ||
const EXPECTED_DIR = join(TEST_DIR, "renamer-expected"); | ||
|
||
function trimTrailingCR(input: string): string { | ||
return input.replace(/\n+$/, ""); | ||
} | ||
|
||
describe("renamer", () => { | ||
it.each(fs.readdirSync(TEST_DIR, { withFileTypes: true }))( | ||
"should have an expected content after being renamed", | ||
(dentry) => { | ||
if (!dentry.isFile()) { | ||
return; | ||
} | ||
const expectedFilePath = join(EXPECTED_DIR, dentry.name); | ||
const expected = fs.readFileSync(expectedFilePath, "utf-8"); | ||
const filePath = join(TEST_DIR, dentry.name); | ||
const src = fs.readFileSync(filePath, "utf-8"); | ||
const inAst = parse(src, filePath, "user"); | ||
const outAst = AstRenamer.make().rename(inAst); | ||
const got = prettyPrint(outAst); | ||
assert.strictEqual( | ||
trimTrailingCR(got), | ||
trimTrailingCR(expected), | ||
`AST comparison after renamed failed for ${dentry.name}`, | ||
); | ||
}, | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import { | ||
AstConstantDef, | ||
AstReceiverKind, | ||
AstModuleItem, | ||
AstStructFieldInitializer, | ||
AstFunctionAttribute, | ||
AstOpBinary, | ||
AstOpUnary, | ||
AstFieldAccess, | ||
AstConditional, | ||
AstMethodCall, | ||
AstStaticCall, | ||
AstNumber, | ||
AstBoolean, | ||
AstString, | ||
AstStructInstance, | ||
AstInitOf, | ||
AstConstantAttribute, | ||
AstContractAttribute, | ||
AstTypedParameter, | ||
AstImport, | ||
AstNativeFunctionDecl, | ||
AstReceiver, | ||
AstStatementRepeat, | ||
AstStatementUntil, | ||
AstStatementWhile, | ||
AstStatementForEach, | ||
AstStatementTry, | ||
AstStatementTryCatch, | ||
AstCondition, | ||
AstStatementAugmentedAssign, | ||
AstStatementAssign, | ||
AstStatementExpression, | ||
AstStatementReturn, | ||
AstStatementLet, | ||
AstFunctionDef, | ||
AstContract, | ||
AstTrait, | ||
AstId, | ||
AstModule, | ||
AstStructDecl, | ||
AstMessageDecl, | ||
AstFunctionDecl, | ||
AstConstantDecl, | ||
AstContractInit, | ||
AstPrimitiveTypeDecl, | ||
AstTypeId, | ||
AstMapType, | ||
AstBouncedMessageType, | ||
AstFieldDecl, | ||
AstOptionalType, | ||
AstNode, | ||
AstFuncId, | ||
} from "./ast"; | ||
import { dummySrcInfo } from "./grammar"; | ||
import { AstHasher, AstHash } from "./hash"; | ||
import { topologicalSort } from "../utils/utils"; | ||
import { throwInternalCompilerError } from "../errors"; | ||
import JSONbig from "json-bigint"; | ||
|
||
type GivenName = string; | ||
|
||
function id(text: string): AstId { | ||
return { kind: "id", text, id: 0, loc: dummySrcInfo }; | ||
} | ||
|
||
/** | ||
* An utility class that provides alpha-renaming and topological sort functionality | ||
* for the AST comparation. | ||
*/ | ||
export class AstRenamer { | ||
private constructor( | ||
private sort: boolean, | ||
private currentIdx: number = 0, | ||
private renamed: Map<AstHash, GivenName> = new Map(), | ||
) {} | ||
public static make(params: Partial<{ sort: boolean }> = {}): AstRenamer { | ||
const { sort = true } = params; | ||
return new AstRenamer(sort); | ||
} | ||
|
||
/** | ||
* Renames the given node based on its AST. | ||
*/ | ||
public rename(node: AstNode): AstNode { | ||
switch (node.kind) { | ||
case "module": | ||
// TODO: Sort imports. Does their order affect the behavior of transitive dependencies? | ||
return { ...node, items: this.renameModuleItems(node.items) }; | ||
default: | ||
throwInternalCompilerError( | ||
`Unsupported node kind: ${node.kind}`, | ||
); | ||
} | ||
} | ||
|
||
private nextIdx(): number { | ||
const value = this.currentIdx; | ||
this.currentIdx += 1; | ||
return value; | ||
} | ||
|
||
/** | ||
* Generates a new unique node name. | ||
*/ | ||
private generateName(node: AstNode): GivenName { | ||
const generate = (prefix: string) => `${prefix}_${this.nextIdx()}`; | ||
switch (node.kind) { | ||
case "struct_decl": | ||
return generate("struct"); | ||
case "message_decl": | ||
return generate("message"); | ||
default: | ||
throwInternalCompilerError(`Unsupported node: ${node.kind}`); | ||
} | ||
} | ||
|
||
/** | ||
* Sets new or an existent name based on node's hash. | ||
*/ | ||
private setName(node: AstNode): GivenName { | ||
const hash = AstHasher.make({ sort: this.sort }).hash(node); | ||
const existentName = this.renamed.get(hash); | ||
if (existentName !== undefined) { | ||
return existentName; | ||
} | ||
const name = this.generateName(node); | ||
this.renamed.set(hash, name); | ||
return name; | ||
} | ||
|
||
public renameModuleItems(items: AstModuleItem[]): AstModuleItem[] { | ||
const primitives = items.filter( | ||
(item) => item.kind === "primitive_type_decl", | ||
); | ||
|
||
// TODO: Rename if they refer to the same FunC function. | ||
const nativeFunctions = items.filter( | ||
(item) => item.kind === "native_function_decl", | ||
); | ||
|
||
// Struct and messages can have other structs as their fields. | ||
// But we don't care; they should have the same name after renaming. | ||
const structs = this.renameItems( | ||
items, | ||
"struct_decl", | ||
this.setName.bind(this), | ||
id, | ||
); | ||
const messages = this.renameItems( | ||
items, | ||
"message_decl", | ||
this.setName.bind(this), | ||
id, | ||
); | ||
|
||
// TODO: Rename | ||
const functions = items.filter((item) => item.kind === "function_def"); | ||
|
||
// TODO: Rename | ||
const constants = items.filter((item) => item.kind === "constant_def"); | ||
|
||
// TODO: Rename | ||
const traits = items.filter((item) => item.kind === "trait"); | ||
|
||
// TODO: Rename | ||
const contracts = items.filter((item) => item.kind === "contract"); | ||
|
||
// TODO: Sort if requested. | ||
return [ | ||
...primitives, | ||
...nativeFunctions, | ||
...structs, | ||
...messages, | ||
...functions, | ||
...constants, | ||
...traits, | ||
...contracts, | ||
]; | ||
} | ||
|
||
private renameItems<T extends { kind: string; name: AstId }>( | ||
items: T[], | ||
targetKind: T["kind"], | ||
setName: (item: T) => string, | ||
id: (newName: string) => AstId, | ||
): T[] { | ||
return items.reduce((acc, item) => { | ||
if (item.kind === targetKind) { | ||
const newName = setName(item); | ||
acc.push({ ...item, name: id(newName) }); | ||
} | ||
return acc; | ||
}, [] as T[]); | ||
} | ||
} |
Oops, something went wrong.