Skip to content

Commit

Permalink
Handle additional edge cases for evalConstantExpr() (#230)
Browse files Browse the repository at this point in the history
* Handle additional edge cases for evalConstantExpr(): support Buffer values (for hex literals) in unary math ops.

* Introduce castToType(). Tweak evalUnary() and evalBinary() to cover more edge-cases. Export utility functions for downstream. Add more edge-cases to tests.

* review nit

---------

Co-authored-by: Dimitar <dimitar.bounov@consensys.net>
  • Loading branch information
blitz-1306 and cd1m0 authored Oct 24, 2023
1 parent d598427 commit 6d3a438
Show file tree
Hide file tree
Showing 3 changed files with 409 additions and 66 deletions.
195 changes: 129 additions & 66 deletions src/types/eval_const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,21 @@ import {
VariableDeclaration
} from "../ast";
import { pp } from "../misc";
import { BytesType, FixedBytesType, IntType, NumericLiteralType, StringType } from "./ast";
import {
BytesType,
FixedBytesType,
IntType,
NumericLiteralType,
StringType,
TypeNode
} from "./ast";
import { InferType } from "./infer";
import { BINARY_OPERATOR_GROUPS, SUBDENOMINATION_MULTIPLIERS, clampIntToType } from "./utils";
import {
BINARY_OPERATOR_GROUPS,
SUBDENOMINATION_MULTIPLIERS,
clampIntToType,
fixedBytesTypeToIntType
} from "./utils";
/**
* Tune up precision of decimal values to follow Solidity behavior.
* Be careful with precision - setting it to large values causes NodeJS to crash.
Expand Down Expand Up @@ -51,7 +63,7 @@ function str(value: Value): string {
return value instanceof Decimal ? value.toString() : pp(value);
}

function promoteToDec(v: Value): Decimal {
export function toDec(v: Value): Decimal {
if (v instanceof Decimal) {
return v;
}
Expand All @@ -71,10 +83,86 @@ function promoteToDec(v: Value): Decimal {
throw new Error(`Expected number not ${v}`);
}

export function toInt(v: Value): bigint {
if (typeof v === "bigint") {
return v;
}

if (v instanceof Decimal && v.isInt()) {
return BigInt(v.toHex());
}

if (typeof v === "string") {
return v === "" ? 0n : BigInt("0x" + Buffer.from(v, "utf-8").toString("hex"));
}

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

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

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

export function castToType(v: Value, fromT: TypeNode | undefined, toT: TypeNode): Value {
if (typeof v === "bigint") {
if (toT instanceof IntType) {
return clampIntToType(v, toT);
}

if (toT instanceof FixedBytesType) {
if (fromT instanceof FixedBytesType && fromT.size < toT.size) {
return BigInt("0x" + v.toString(16).padEnd(toT.size * 2, "0"));
}

return clampIntToType(v, fixedBytesTypeToIntType(toT));
}
}

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

if (toT instanceof FixedBytesType) {
if (v.length === 0) {
return 0n;
}

const buf = Buffer.from(v, "utf-8");

if (buf.length < toT.size) {
return BigInt("0x" + buf.toString("hex").padEnd(toT.size * 2, "0"));
}

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

if (v instanceof Buffer) {
if (toT instanceof StringType) {
return v.toString("utf-8");
}

if (toT instanceof FixedBytesType) {
if (v.length === 0) {
return 0n;
}

if (v.length < toT.size) {
return BigInt("0x" + v.toString("hex").padEnd(toT.size * 2, "0"));
}

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

return v;
}

export function isConstant(expr: Expression | VariableDeclaration): boolean {
if (expr instanceof Literal) {
return true;
Expand Down Expand Up @@ -259,8 +347,8 @@ export function evalBinaryImpl(operator: string, left: Value, right: Value): Val
if (typeof left === "boolean" || typeof right === "boolean") {
isEqual = left === right;
} else {
const leftDec = promoteToDec(left);
const rightDec = promoteToDec(right);
const leftDec = toDec(left);
const rightDec = toDec(right);

isEqual = leftDec.equals(rightDec);
}
Expand All @@ -283,8 +371,8 @@ export function evalBinaryImpl(operator: string, left: Value, right: Value): Val
);
}

const leftDec = promoteToDec(left);
const rightDec = promoteToDec(right);
const leftDec = toDec(left);
const rightDec = toDec(right);

if (operator === "<") {
return leftDec.lessThan(rightDec);
Expand All @@ -306,8 +394,8 @@ export function evalBinaryImpl(operator: string, left: Value, right: Value): Val
}

if (BINARY_OPERATOR_GROUPS.Arithmetic.includes(operator)) {
const leftDec = promoteToDec(left);
const rightDec = promoteToDec(right);
const leftDec = toDec(left);
const rightDec = toDec(right);

let res: Decimal;

Expand All @@ -331,28 +419,27 @@ export function evalBinaryImpl(operator: string, left: Value, right: Value): Val
}

if (BINARY_OPERATOR_GROUPS.Bitwise.includes(operator)) {
if (!(typeof left === "bigint" && typeof right === "bigint")) {
throw new EvalError(`${operator} expects integers not ${str(left)} and ${str(right)}`);
}
const leftInt = toInt(left);
const rightInt = toInt(right);

if (operator === "<<") {
return left << right;
return leftInt << rightInt;
}

if (operator === ">>") {
return left >> right;
return leftInt >> rightInt;
}

if (operator === "|") {
return left | right;
return leftInt | rightInt;
}

if (operator === "&") {
return left & right;
return leftInt & rightInt;
}

if (operator === "^") {
return left ^ right;
return leftInt ^ rightInt;
}

throw new EvalError(`Unknown bitwise operator ${operator}`);
Expand Down Expand Up @@ -396,13 +483,16 @@ export function evalLiteral(node: Literal): Value {
export function evalUnary(node: UnaryOperation, inference: InferType): Value {
try {
const subT = inference.typeOf(node.vSubExpression);
const res = evalUnaryImpl(node.operator, evalConstantExpr(node.vSubExpression, inference));
const sub = evalConstantExpr(node.vSubExpression, inference);

if (subT instanceof IntType && typeof res === "bigint") {
return clampIntToType(res, subT);
if (subT instanceof NumericLiteralType) {
return evalUnaryImpl(node.operator, sub);
}

return res;
const resT = inference.typeOfUnaryOperation(node);
const res = evalUnaryImpl(node.operator, sub);

return castToType(res, undefined, resT);
} catch (e: unknown) {
if (e instanceof EvalError && e.expr === undefined) {
e.expr = node;
Expand All @@ -417,21 +507,25 @@ export function evalBinary(node: BinaryOperation, inference: InferType): Value {
const leftT = inference.typeOf(node.vLeftExpression);
const rightT = inference.typeOf(node.vRightExpression);

const res = evalBinaryImpl(
node.operator,
evalConstantExpr(node.vLeftExpression, inference),
evalConstantExpr(node.vRightExpression, inference)
);
let left = evalConstantExpr(node.vLeftExpression, inference);
let right = evalConstantExpr(node.vRightExpression, inference);

if (!(leftT instanceof NumericLiteralType && rightT instanceof NumericLiteralType)) {
const resT = inference.typeOfBinaryOperation(node);
if (leftT instanceof NumericLiteralType && rightT instanceof NumericLiteralType) {
return evalBinaryImpl(node.operator, left, right);
}

if (resT instanceof IntType && typeof res === "bigint") {
return clampIntToType(res, resT);
}
if (node.operator !== "**" && node.operator !== ">>" && node.operator !== "<<") {
const commonT = inference.inferCommonType(leftT, rightT);

left = castToType(left, leftT, commonT);
right = castToType(right, rightT, commonT);
}

return res;
const res = evalBinaryImpl(node.operator, left, right);

const resT = inference.typeOfBinaryOperation(node);

return castToType(res, undefined, resT);
} catch (e: unknown) {
if (e instanceof EvalError && e.expr === undefined) {
e.expr = node;
Expand Down Expand Up @@ -503,41 +597,10 @@ export function evalFunctionCall(node: FunctionCall, inference: InferType): Valu
}

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 (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 (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"));
}
}
const fromT = inference.typeOf(node.vArguments[0]);
const toT = inference.typeOfElementaryTypeNameExpression(node.vExpression).type;

return val;
return castToType(val, fromT, toT);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ export function enumToIntType(decl: EnumDefinition): IntType {
return new IntType(size, false);
}

export function fixedBytesTypeToIntType(type: FixedBytesType): IntType {
return new IntType(type.size * 8, false, type.src);
}

export function getABIEncoderVersion(unit: SourceUnit, compilerVersion: string): ABIEncoderVersion {
const predefined = unit.abiEncoderVersion;

Expand Down
Loading

0 comments on commit 6d3a438

Please sign in to comment.