Skip to content

Commit

Permalink
feat(cmp): Introduce basic alpha renaming for top-level struct/messages
Browse files Browse the repository at this point in the history
  • Loading branch information
jubnzv committed Aug 15, 2024
1 parent d0c5eba commit 48251c8
Show file tree
Hide file tree
Showing 17 changed files with 769 additions and 3 deletions.
17 changes: 14 additions & 3 deletions src/grammar/compare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AstConstantDef,
AstReceiverKind,
AstModuleItem,
AstStructFieldInitializer,
AstFunctionAttribute,
AstOpBinary,
Expand Down Expand Up @@ -51,16 +52,20 @@ import {
AstNode,
AstFuncId,
} from "./ast";
import JSONbig from "json-bigint";
import { AstRenamer } from "./rename";
import { throwInternalCompilerError } from "../errors";
import JSONbig from "json-bigint";

/**
* Provides an API to compare two AST nodes with extra options.
*/
export class AstComparator {
/**
* @param sort Topologically sort AST entries before comparing.
* @param canonialize Introduce de Brujin indicies for local bindings to handle duplicate code with different names.
* @param sort Topologically sort AST entries before comparing. Should be enabled
* in order to handle duplicate entries shuffled in the source code.
* @param canonialize Introduce de Brujin indicies for local bindings to handle
* duplicate code with different names. Should be enabled in order to
* treat duplicate entries with different names as the same elements.
*/
private constructor(
private readonly sort: boolean,
Expand All @@ -79,6 +84,12 @@ export class AstComparator {
return false;
}

if (this.canonialize === true) {
const renamer = AstRenamer.make({ sort: this.sort });
node1 = renamer.rename(node1);
node2 = renamer.rename(node2);
}

switch (node1.kind) {
case "module": {
const { imports: imports1, items: items1 } = node1 as AstModule;
Expand Down
117 changes: 117 additions & 0 deletions src/grammar/hash.ts
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;
}
}
}
37 changes: 37 additions & 0 deletions src/grammar/rename.spec.ts
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}`,
);
},
);
});
196 changes: 196 additions & 0 deletions src/grammar/rename.ts
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[]);
}
}
Loading

0 comments on commit 48251c8

Please sign in to comment.