From ac2dfc8fafc42f3fe3c820f89a8803f5eb7b9e57 Mon Sep 17 00:00:00 2001 From: blitz-1306 Date: Fri, 6 Oct 2023 17:26:43 +0500 Subject: [PATCH 1/4] Tweak logic for better constant expression evaluation (respect file-level constants, support more node types and cast operations) --- src/types/eval_const.ts | 162 +++++++++++++++++++----- test/integration/eval_const.spec.ts | 101 +++++++++++++++ test/samples/solidity/consts/consts.sol | 22 ++++ test/samples/solidity/consts/lib_a.sol | 5 + test/samples/solidity/consts/lib_b.sol | 3 + test/unit/types/eval_const.spec.ts | 60 ++++++++- 6 files changed, 319 insertions(+), 34 deletions(-) create mode 100644 test/integration/eval_const.spec.ts create mode 100644 test/samples/solidity/consts/consts.sol create mode 100644 test/samples/solidity/consts/lib_a.sol create mode 100644 test/samples/solidity/consts/lib_b.sol diff --git a/src/types/eval_const.ts b/src/types/eval_const.ts index 40a08af6..5b3d8b60 100644 --- a/src/types/eval_const.ts +++ b/src/types/eval_const.ts @@ -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"; /** @@ -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; @@ -62,6 +64,10 @@ 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}`); } @@ -69,7 +75,7 @@ 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; } @@ -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) && @@ -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 ( @@ -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) { @@ -408,22 +425,93 @@ 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; + + 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" && node.vExpression instanceof ElementaryTypeNameExpression) { - const castT = inference.typeOfElementaryTypeNameExpression(node.vExpression); - const toT = castT.type; + if (typeof val === "bigint") { + if (castT instanceof IntType) { + return clampIntToType(val, castT); + } - if (toT instanceof IntType) { - return clampIntToType(val, toT); + if (castT instanceof FixedBytesType) { + return val; + } + } + + 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")); } } @@ -437,7 +525,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); } @@ -464,12 +555,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 (decl instanceof VariableDeclaration) { - return evalConstantExpr(decl.vValue as Expression, inference); - } + if (node instanceof Identifier || node instanceof MemberAccess) { + return evalConstantExpr( + node.vReferencedDeclaration as Expression | VariableDeclaration, + inference + ); + } + + if (node instanceof IndexAccess) { + return evalIndexAccess(node, inference); } if (node instanceof FunctionCall) { diff --git a/test/integration/eval_const.spec.ts b/test/integration/eval_const.spec.ts new file mode 100644 index 00000000..893375f4 --- /dev/null +++ b/test/integration/eval_const.spec.ts @@ -0,0 +1,101 @@ +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[@id=5]", 100n], + ["//VariableDeclaration[@id=8]", 15n], + ["//VariableDeclaration[@id=13]", 115n], + ["//VariableDeclaration[@id=18]", 158n], + ["//VariableDeclaration[@id=24]", 158n], + ["//VariableDeclaration[@id=31]", false], + ["//VariableDeclaration[@id=37]", 158n], + ["//VariableDeclaration[@id=44]", 85n], + ["//VariableDeclaration[@id=47]", "abcd"], + ["//VariableDeclaration[@id=53]", Buffer.from("abcd", "utf-8")], + ["//VariableDeclaration[@id=58]", 97n], + ["//VariableDeclaration[@id=64]", "abcd"], + ["//VariableDeclaration[@id=73]", 30841n], + ["//VariableDeclaration[@id=82]", 30841n], + ["//VariableDeclaration[@id=88]", 258n] + ] + ] +]; + +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 + ); + }); + } + }); + } +}); diff --git a/test/samples/solidity/consts/consts.sol b/test/samples/solidity/consts/consts.sol new file mode 100644 index 00000000..0dee088a --- /dev/null +++ b/test/samples/solidity/consts/consts.sol @@ -0,0 +1,22 @@ +import "./lib_a.sol"; +import "./lib_a.sol" as LibA; + +uint constant SOME_CONST = 100; +uint constant SOME_OTHER = 15; +uint constant SOME_ELSE = SOME_CONST + SOME_OTHER; +uint constant C2 = SOME_ELSE + ANOTHER_CONST; +uint constant C3 = SOME_ELSE + LibA.ANOTHER_CONST; +uint constant C4 = -SOME_CONST; +bool constant C5 = false; +uint constant C6 = C5 ? SOME_ELSE : C3; +uint constant C7 = LibA.ANOTHER_CONST + LibB.AND_ANOTHER_CONST; +// uint constant C8 = LibA.ANOTHER_CONST + LibA.LibB.AND_ANOTHER_CONST; + +string constant FOO = "abcd"; +bytes constant BOO = bytes("abcd"); +bytes1 constant MOO = BOO[0]; +string constant WOO = string(BOO); + +uint16 constant U16S = uint16(bytes2("xy")); +uint16 constant U16B = uint16(bytes2(hex"7879")); +bytes2 constant B2U = bytes2(0x0102); diff --git a/test/samples/solidity/consts/lib_a.sol b/test/samples/solidity/consts/lib_a.sol new file mode 100644 index 00000000..ddedaf32 --- /dev/null +++ b/test/samples/solidity/consts/lib_a.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.7.5; + +import "./lib_b.sol" as LibB; + +uint constant ANOTHER_CONST = LibB.AND_ANOTHER_CONST + 1; diff --git a/test/samples/solidity/consts/lib_b.sol b/test/samples/solidity/consts/lib_b.sol new file mode 100644 index 00000000..e48f3e46 --- /dev/null +++ b/test/samples/solidity/consts/lib_b.sol @@ -0,0 +1,3 @@ +pragma solidity ^0.7.5; + +uint constant AND_ANOTHER_CONST = 42; diff --git a/test/unit/types/eval_const.spec.ts b/test/unit/types/eval_const.spec.ts index 3f65e61a..bf424f08 100644 --- a/test/unit/types/eval_const.spec.ts +++ b/test/unit/types/eval_const.spec.ts @@ -62,9 +62,9 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va [ "Literal (hex string)", (factory: ASTNodeFactory) => - factory.makeLiteral("", LiteralKind.HexString, "ffcc33", "abcdef"), + factory.makeLiteral("", LiteralKind.HexString, "888990", "xyz"), true, - BigInt("0xffcc33") + Buffer.from("888990", "hex") ], [ "Literal (uint8)", @@ -1300,6 +1300,62 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va ), true, 0n + ], + [ + 'Identifier & IndexAccess (const A = "abcdef" (string), a[2])', + (factory: ASTNodeFactory) => { + const v = factory.makeVariableDeclaration( + true, + false, + "A", + 0, + true, + DataLocation.Default, + StateVariableVisibility.Public, + Mutability.Constant, + "string", + undefined, + factory.makeElementaryTypeName("string", "string"), + undefined, + factory.makeLiteral("", LiteralKind.String, "", "abcdef") + ); + + return factory.makeIndexAccess( + "string", + factory.makeIdentifierFor(v), + factory.makeLiteral("", LiteralKind.Number, "", "2") + ); + }, + true, + undefined + ], + [ + "Identifier & IndexAccess (const A = 0xab_cd_ef (bytes3), a[1])", + (factory: ASTNodeFactory) => { + const v = factory.makeVariableDeclaration( + true, + false, + "A", + 0, + true, + DataLocation.Default, + StateVariableVisibility.Public, + Mutability.Constant, + "bytes3", + undefined, + factory.makeElementaryTypeName("bytes3", "bytes3"), + undefined, + factory.makeLiteral("", LiteralKind.Number, "", "0xab_cd_ef") + ); + + return factory.makeIndexAccess( + "string", + factory.makeIdentifierFor(v), + factory.makeLiteral("", LiteralKind.Number, "", "1") + ); + }, + true, + BigInt("0xcd") ] ]; From e4b1068660a8535afed491e6873c813822760456 Mon Sep 17 00:00:00 2001 From: blitz-1306 Date: Fri, 6 Oct 2023 17:55:01 +0500 Subject: [PATCH 2/4] Tweak integration test to refer to variable names for clarity --- test/integration/eval_const.spec.ts | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/test/integration/eval_const.spec.ts b/test/integration/eval_const.spec.ts index 893375f4..5f5115b9 100644 --- a/test/integration/eval_const.spec.ts +++ b/test/integration/eval_const.spec.ts @@ -18,21 +18,27 @@ const cases: Array<[string, Array<[string, Value]>]> = [ [ "test/samples/solidity/consts/consts.sol", [ - ["//VariableDeclaration[@id=5]", 100n], - ["//VariableDeclaration[@id=8]", 15n], - ["//VariableDeclaration[@id=13]", 115n], - ["//VariableDeclaration[@id=18]", 158n], - ["//VariableDeclaration[@id=24]", 158n], - ["//VariableDeclaration[@id=31]", false], - ["//VariableDeclaration[@id=37]", 158n], - ["//VariableDeclaration[@id=44]", 85n], - ["//VariableDeclaration[@id=47]", "abcd"], - ["//VariableDeclaration[@id=53]", Buffer.from("abcd", "utf-8")], - ["//VariableDeclaration[@id=58]", 97n], - ["//VariableDeclaration[@id=64]", "abcd"], - ["//VariableDeclaration[@id=73]", 30841n], - ["//VariableDeclaration[@id=82]", 30841n], - ["//VariableDeclaration[@id=88]", 258n] + ["//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] ] ] ]; From 4fe0749d044bd585c6afeed0f40c27f1e3351ea5 Mon Sep 17 00:00:00 2001 From: blitz-1306 Date: Mon, 9 Oct 2023 10:19:10 +0500 Subject: [PATCH 3/4] Handle string literals with byte sequence in constant expression evaluator (#227) --- src/types/eval_const.ts | 26 ++++++++++++++++++++----- test/integration/eval_const.spec.ts | 3 ++- test/samples/solidity/consts/consts.sol | 2 ++ test/unit/types/eval_const.spec.ts | 9 ++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/types/eval_const.ts b/src/types/eval_const.ts index 5b3d8b60..e8a69a94 100644 --- a/src/types/eval_const.ts +++ b/src/types/eval_const.ts @@ -362,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; diff --git a/test/integration/eval_const.spec.ts b/test/integration/eval_const.spec.ts index 5f5115b9..f57782ab 100644 --- a/test/integration/eval_const.spec.ts +++ b/test/integration/eval_const.spec.ts @@ -38,7 +38,8 @@ const cases: Array<[string, Array<[string, Value]>]> = [ ["//VariableDeclaration[@name='U16S']", 30841n], ["//VariableDeclaration[@name='U16B']", 30841n], - ["//VariableDeclaration[@name='B2U']", 258n] + ["//VariableDeclaration[@name='B2U']", 258n], + ["//VariableDeclaration[@name='NON_UTF8_SEQ']", Buffer.from("7532eaac", "hex")] ] ] ]; diff --git a/test/samples/solidity/consts/consts.sol b/test/samples/solidity/consts/consts.sol index 0dee088a..a53ebd5a 100644 --- a/test/samples/solidity/consts/consts.sol +++ b/test/samples/solidity/consts/consts.sol @@ -20,3 +20,5 @@ string constant WOO = string(BOO); uint16 constant U16S = uint16(bytes2("xy")); uint16 constant U16B = uint16(bytes2(hex"7879")); bytes2 constant B2U = bytes2(0x0102); + +bytes4 constant NON_UTF8_SEQ = "\x75\x32\xea\xac"; diff --git a/test/unit/types/eval_const.spec.ts b/test/unit/types/eval_const.spec.ts index bf424f08..e78507dd 100644 --- a/test/unit/types/eval_const.spec.ts +++ b/test/unit/types/eval_const.spec.ts @@ -25,7 +25,7 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va undefined ], [ - "Literal (unknown)", + "Literal (invalid kind)", (factory: ASTNodeFactory) => factory.makeLiteral("", "unknown" as LiteralKind, "", "???"), true, @@ -66,6 +66,13 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va true, Buffer.from("888990", "hex") ], + [ + "Literal (invalid UTF-8 sequence edge case)", + (factory: ASTNodeFactory) => + factory.makeLiteral("", LiteralKind.String, "7532eaac", null as any), + true, + Buffer.from("7532eaac", "hex") + ], [ "Literal (uint8)", (factory: ASTNodeFactory) => From b9b54ff7e1695abf0ed52241b06b43d67869e6d3 Mon Sep 17 00:00:00 2001 From: blitz-1306 Date: Tue, 10 Oct 2023 10:23:16 +0500 Subject: [PATCH 4/4] Address review remarks: check for OOB when index accessing into a fixed bytes, clamp int to type on cast to bytes. --- src/types/eval_const.ts | 8 +++++- test/unit/types/eval_const.spec.ts | 40 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/types/eval_const.ts b/src/types/eval_const.ts index e8a69a94..2f2fcef1 100644 --- a/src/types/eval_const.ts +++ b/src/types/eval_const.ts @@ -463,6 +463,12 @@ export function evalIndexAccess(node: IndexAccess, inference: InferType): Value 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)); } @@ -505,7 +511,7 @@ export function evalFunctionCall(node: FunctionCall, inference: InferType): Valu } if (castT instanceof FixedBytesType) { - return val; + return clampIntToType(val, new IntType(castT.size * 8, false)); } } diff --git a/test/unit/types/eval_const.spec.ts b/test/unit/types/eval_const.spec.ts index e78507dd..841fcc86 100644 --- a/test/unit/types/eval_const.spec.ts +++ b/test/unit/types/eval_const.spec.ts @@ -1308,6 +1308,18 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va true, 0n ], + [ + "Edge-case <0.5.0: bytes1(1000)", + (factory: ASTNodeFactory) => + factory.makeFunctionCall( + "bytes1", + FunctionCallKind.TypeConversion, + factory.makeElementaryTypeNameExpression("type(bytes1)", "bytes1"), + [factory.makeLiteral("int_const 1000", LiteralKind.Number, "", "1000")] + ), + true, + BigInt("0xe8") + ], [ 'Identifier & IndexAccess (const A = "abcdef" (string), a[2])', (factory: ASTNodeFactory) => { @@ -1363,6 +1375,34 @@ const cases: Array<[string, (factory: ASTNodeFactory) => Expression, boolean, Va }, true, BigInt("0xcd") + ], + [ + "Identifier & IndexAccess (const A = 0xab_cd_ef (bytes3), a[3])", + (factory: ASTNodeFactory) => { + const v = factory.makeVariableDeclaration( + true, + false, + "A", + 0, + true, + DataLocation.Default, + StateVariableVisibility.Public, + Mutability.Constant, + "bytes3", + undefined, + factory.makeElementaryTypeName("bytes3", "bytes3"), + undefined, + factory.makeLiteral("", LiteralKind.Number, "", "0xab_cd_ef") + ); + + return factory.makeIndexAccess( + "string", + factory.makeIdentifierFor(v), + factory.makeLiteral("", LiteralKind.Number, "", "3") + ); + }, + true, + undefined ] ];