Skip to content

Commit

Permalink
Improve constant expression evaluator (#225)
Browse files Browse the repository at this point in the history
* Tweak logic for better constant expression evaluation (respect file-level constants, support more node types and cast operations)

* Tweak integration test to refer to variable names for clarity

* Handle string literals with byte sequence in constant expression evaluator (#227)

* Address review remarks: check for OOB when index accessing into a fixed bytes, clamp int to type on cast to bytes.
  • Loading branch information
blitz-1306 authored Oct 13, 2023
1 parent f467859 commit 883a401
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 40 deletions.
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;

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

0 comments on commit 883a401

Please sign in to comment.