Skip to content
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

Improve constant expression evaluator #225

Merged
merged 4 commits into from
Oct 13, 2023
Merged
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
194 changes: 157 additions & 37 deletions src/types/eval_const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import {
FunctionCall,
FunctionCallKind,
Identifier,
IndexAccess,
Literal,
LiteralKind,
MemberAccess,
TimeUnit,
TupleExpression,
UnaryOperation,
VariableDeclaration
} from "../ast";
import { assert, pp } from "../misc";
import { IntType, NumericLiteralType } from "./ast";
import { pp } from "../misc";
import { BytesType, FixedBytesType, IntType, NumericLiteralType, StringType } from "./ast";
import { InferType } from "./infer";
import { BINARY_OPERATOR_GROUPS, SUBDENOMINATION_MULTIPLIERS, clampIntToType } from "./utils";
/**
Expand All @@ -27,7 +29,7 @@ import { BINARY_OPERATOR_GROUPS, SUBDENOMINATION_MULTIPLIERS, clampIntToType } f
*/
Decimal.set({ precision: 100 });

export type Value = Decimal | boolean | string | bigint;
export type Value = Decimal | boolean | string | bigint | Buffer;

export class EvalError extends Error {
expr?: Expression;
Expand Down Expand Up @@ -62,14 +64,18 @@ function promoteToDec(v: Value): Decimal {
return new Decimal(v === "" ? 0 : "0x" + Buffer.from(v, "utf-8").toString("hex"));
}

if (v instanceof Buffer) {
return new Decimal(v.length === 0 ? 0 : "0x" + v.toString("hex"));
}

throw new Error(`Expected number not ${v}`);
}

function demoteFromDec(d: Decimal): Decimal | bigint {
return d.isInt() ? BigInt(d.toFixed()) : d;
}

export function isConstant(expr: Expression): boolean {
export function isConstant(expr: Expression | VariableDeclaration): boolean {
if (expr instanceof Literal) {
return true;
}
Expand All @@ -78,6 +84,15 @@ export function isConstant(expr: Expression): boolean {
return true;
}

if (
expr instanceof VariableDeclaration &&
expr.constant &&
expr.vValue &&
isConstant(expr.vValue)
) {
return true;
}

if (
expr instanceof BinaryOperation &&
isConstant(expr.vLeftExpression) &&
Expand Down Expand Up @@ -108,17 +123,19 @@ export function isConstant(expr: Expression): boolean {
return true;
}

if (expr instanceof Identifier) {
const decl = expr.vReferencedDeclaration;
if (expr instanceof Identifier || expr instanceof MemberAccess) {
return (
expr.vReferencedDeclaration instanceof VariableDeclaration &&
isConstant(expr.vReferencedDeclaration)
);
}

if (
decl instanceof VariableDeclaration &&
decl.constant &&
decl.vValue &&
isConstant(decl.vValue)
) {
return true;
}
if (expr instanceof IndexAccess) {
return (
isConstant(expr.vBaseExpression) &&
expr.vIndexExpression !== undefined &&
isConstant(expr.vIndexExpression)
);
}

if (
Expand All @@ -142,7 +159,7 @@ export function evalLiteralImpl(
}

if (kind === LiteralKind.HexString) {
return value === "" ? 0n : BigInt("0x" + value);
return Buffer.from(value, "hex");
}

if (kind === LiteralKind.String || kind === LiteralKind.UnicodeString) {
Expand Down Expand Up @@ -345,12 +362,28 @@ export function evalBinaryImpl(operator: string, left: Value, right: Value): Val
}

export function evalLiteral(node: Literal): Value {
let kind = node.kind;

/**
* An example:
*
* ```solidity
* contract Test {
* bytes4 constant s = "\x75\x32\xea\xac";
* }
* ```
*
* Note that compiler leaves "null" as string value,
* so we have to rely on hexadecimal representation instead.
*/
if ((kind === LiteralKind.String || kind === LiteralKind.UnicodeString) && node.value == null) {
kind = LiteralKind.HexString;
}

const value = kind === LiteralKind.HexString ? node.hexValue : node.value;

try {
return evalLiteralImpl(
node.kind,
node.kind === LiteralKind.HexString ? node.hexValue : node.value,
node.subdenomination
);
return evalLiteralImpl(kind, value, node.subdenomination);
} catch (e: unknown) {
if (e instanceof EvalError) {
e.expr = node;
Expand Down Expand Up @@ -408,22 +441,99 @@ export function evalBinary(node: BinaryOperation, inference: InferType): Value {
}
}

export function evalIndexAccess(node: IndexAccess, inference: InferType): Value {
const base = evalConstantExpr(node.vBaseExpression, inference);
const index = evalConstantExpr(node.vIndexExpression as Expression, inference);

if (!(typeof index === "bigint" || index instanceof Decimal)) {
throw new EvalError(
`Unexpected non-numeric index into base in expression ${pp(node)}`,
node
);
}

const plainIndex = index instanceof Decimal ? index.toNumber() : Number(index);

if (typeof base === "bigint" || base instanceof Decimal) {
let baseHex = base instanceof Decimal ? base.toHex().slice(2) : base.toString(16);

if (baseHex.length % 2 !== 0) {
baseHex = "0" + baseHex;
}

const indexInHex = plainIndex * 2;
cd1m0 marked this conversation as resolved.
Show resolved Hide resolved

if (indexInHex >= baseHex.length) {
throw new EvalError(
`Out-of-bounds index access ${indexInHex} (originally ${plainIndex}) to "${baseHex}"`
);
}

return BigInt("0x" + baseHex.slice(indexInHex, indexInHex + 2));
}

if (base instanceof Buffer) {
const res = base.at(plainIndex);

if (res === undefined) {
throw new EvalError(
`Out-of-bounds index access ${plainIndex} to ${base.toString("hex")}`
);
}

return BigInt(res);
}

throw new EvalError(`Unable to process ${pp(node)}`, node);
}

export function evalFunctionCall(node: FunctionCall, inference: InferType): Value {
assert(
node.kind === FunctionCallKind.TypeConversion,
'Expected constant call to be a "{0}", but got "{1}" instead',
FunctionCallKind.TypeConversion,
node.kind
);
if (node.kind !== FunctionCallKind.TypeConversion) {
throw new EvalError(
`Expected function call to have kind "${FunctionCallKind.TypeConversion}", but got "${node.kind}" instead`,
node
);
}

if (!(node.vExpression instanceof ElementaryTypeNameExpression)) {
throw new EvalError(
`Expected function call expression to be an ${ElementaryTypeNameExpression.name}, but got "${node.type}" instead`,
node
);
}

const val = evalConstantExpr(node.vArguments[0], inference);
const castT = inference.typeOfElementaryTypeNameExpression(node.vExpression).type;

if (typeof val === "bigint") {
if (castT instanceof IntType) {
return clampIntToType(val, castT);
}

if (typeof val === "bigint" && node.vExpression instanceof ElementaryTypeNameExpression) {
const castT = inference.typeOfElementaryTypeNameExpression(node.vExpression);
const toT = castT.type;
if (castT instanceof FixedBytesType) {
return clampIntToType(val, new IntType(castT.size * 8, false));
}
}

if (typeof val === "string") {
if (castT instanceof BytesType) {
return Buffer.from(val, "utf-8");
}

if (castT instanceof FixedBytesType) {
const buf = Buffer.from(val, "utf-8");

return BigInt("0x" + buf.slice(0, castT.size).toString("hex"));
}
}

if (toT instanceof IntType) {
return clampIntToType(val, toT);
if (val instanceof Buffer) {
if (castT instanceof StringType) {
return val.toString("utf-8");
}

if (castT instanceof FixedBytesType) {
return BigInt("0x" + val.slice(0, castT.size).toString("hex"));
}
}

Expand All @@ -437,7 +547,10 @@ export function evalFunctionCall(node: FunctionCall, inference: InferType): Valu
* @todo The order of some operations changed in some version.
* Current implementation does not yet take it into an account.
*/
export function evalConstantExpr(node: Expression, inference: InferType): Value {
export function evalConstantExpr(
node: Expression | VariableDeclaration,
inference: InferType
): Value {
if (!isConstant(node)) {
throw new NonConstantExpressionError(node);
}
Expand All @@ -464,12 +577,19 @@ export function evalConstantExpr(node: Expression, inference: InferType): Value
: evalConstantExpr(node.vFalseExpression, inference);
}

if (node instanceof Identifier) {
const decl = node.vReferencedDeclaration;
if (node instanceof VariableDeclaration) {
return evalConstantExpr(node.vValue as Expression, inference);
}

if (node instanceof Identifier || node instanceof MemberAccess) {
return evalConstantExpr(
node.vReferencedDeclaration as Expression | VariableDeclaration,
inference
);
}

if (decl instanceof VariableDeclaration) {
return evalConstantExpr(decl.vValue as Expression, inference);
}
if (node instanceof IndexAccess) {
return evalIndexAccess(node, inference);
}

if (node instanceof FunctionCall) {
Expand Down
108 changes: 108 additions & 0 deletions test/integration/eval_const.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import expect from "expect";
import {
assert,
ASTReader,
compileSol,
detectCompileErrors,
evalConstantExpr,
Expression,
InferType,
LatestCompilerVersion,
SourceUnit,
Value,
VariableDeclaration,
XPath
} from "../../src";

const cases: Array<[string, Array<[string, Value]>]> = [
[
"test/samples/solidity/consts/consts.sol",
[
["//VariableDeclaration[@name='SOME_CONST']", 100n],
["//VariableDeclaration[@name='SOME_OTHER']", 15n],
["//VariableDeclaration[@name='SOME_ELSE']", 115n],
["//VariableDeclaration[@name='C2']", 158n],
["//VariableDeclaration[@name='C3']", 158n],
[
"//VariableDeclaration[@name='C4']",
115792089237316195423570985008687907853269984665640564039457584007913129639836n
],
["//VariableDeclaration[@name='C5']", false],
["//VariableDeclaration[@name='C6']", 158n],
["//VariableDeclaration[@name='C7']", 85n],

["//VariableDeclaration[@name='FOO']", "abcd"],
["//VariableDeclaration[@name='BOO']", Buffer.from("abcd", "utf-8")],
["//VariableDeclaration[@name='MOO']", 97n],
["//VariableDeclaration[@name='WOO']", "abcd"],

["//VariableDeclaration[@name='U16S']", 30841n],
["//VariableDeclaration[@name='U16B']", 30841n],
["//VariableDeclaration[@name='B2U']", 258n],
["//VariableDeclaration[@name='NON_UTF8_SEQ']", Buffer.from("7532eaac", "hex")]
]
]
];

describe("Constant expression evaluator integration test", () => {
for (const [sample, mapping] of cases) {
describe(sample, () => {
let units: SourceUnit[];
let inference: InferType;

before(async () => {
const result = await compileSol(sample, "auto");

const data = result.data;
const compilerVersion = result.compilerVersion || LatestCompilerVersion;

const errors = detectCompileErrors(data);

expect(errors).toHaveLength(0);

const reader = new ASTReader();

units = reader.read(data);

expect(units.length).toBeGreaterThanOrEqual(1);

inference = new InferType(compilerVersion);
});

for (const [selector, expectation] of mapping) {
let found = false;

it(`${selector} -> ${expectation}`, () => {
for (const unit of units) {
const results = new XPath(unit).query(selector);

if (results.length > 0) {
const [expr] = results;

assert(
expr instanceof Expression || expr instanceof VariableDeclaration,
`Expected selector result to be an {0} or {1} descendant, got {2} instead`,
Expression.name,
VariableDeclaration.name,
expr
);

found = true;

expect(evalConstantExpr(expr, inference)).toEqual(expectation);

break;
}
}

assert(
found,
`Selector "{0}" not found in source units of sample "{1}"`,
selector,
sample
);
});
}
});
}
});
Loading