Skip to content

Commit

Permalink
Merge pull request #323 from multiversx/counted-variadic
Browse files Browse the repository at this point in the history
Non-breaking change: add support for "counted-variadic"
  • Loading branch information
andreibancioiu authored Aug 31, 2023
2 parents 836222b + 56edcd6 commit d8b4893
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 96 deletions.
98 changes: 66 additions & 32 deletions src/smartcontracts/argSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ARGUMENTS_SEPARATOR } from "../constants";
import { BinaryCodec } from "./codec";
import { EndpointParameterDefinition, Type, TypedValue } from "./typesystem";
import { EndpointParameterDefinition, Type, TypedValue, U32Type, U32Value } from "./typesystem";
import { OptionalType, OptionalValue } from "./typesystem/algebraic";
import { CompositeType, CompositeValue } from "./typesystem/composite";
import { VariadicType, VariadicValue } from "./typesystem/variadic";
Expand Down Expand Up @@ -68,33 +68,48 @@ export class ArgSerializer {

// This is a recursive function.
function readValue(type: Type): TypedValue {
// TODO: Use matchers.

if (type.hasExactClass(OptionalType.ClassName)) {
let typedValue = readValue(type.getFirstTypeParameter());
const typedValue = readValue(type.getFirstTypeParameter());
return new OptionalValue(type, typedValue);
} else if (type.hasExactClass(VariadicType.ClassName)) {
let typedValues = [];
}

while (!hasReachedTheEnd()) {
typedValues.push(readValue(type.getFirstTypeParameter()));
}
if (type.hasExactClass(VariadicType.ClassName)) {
return readVariadicValue(type);
}

return new VariadicValue(type, typedValues);
} else if (type.hasExactClass(CompositeType.ClassName)) {
let typedValues = [];
if (type.hasExactClass(CompositeType.ClassName)) {
const typedValues = [];

for (const typeParameter of type.getTypeParameters()) {
typedValues.push(readValue(typeParameter));
}

return new CompositeValue(type, typedValues);
}

// Non-composite (singular), non-variadic (fixed) type.
// The only branching without a recursive call.
const typedValue = decodeNextBuffer(type);
return typedValue!;
}

function readVariadicValue(type: Type): TypedValue {
const variadicType = <VariadicType>type;
const typedValues = [];

if (variadicType.isCounted) {
const count: number = readValue(new U32Type()).valueOf().toNumber();

for (let i = 0; i < count; i++) {
typedValues.push(readValue(type.getFirstTypeParameter()));
}
} else {
// Non-composite (singular), non-variadic (fixed) type.
// The only branching without a recursive call.
let typedValue = decodeNextBuffer(type);
return typedValue!;
while (!hasReachedTheEnd()) {
typedValues.push(readValue(type.getFirstTypeParameter()));
}
}

return new VariadicValue(variadicType, typedValues);
}

function decodeNextBuffer(type: Type): TypedValue | null {
Expand Down Expand Up @@ -141,36 +156,55 @@ export class ArgSerializer {
// TODO: Refactor, split (function is quite complex).
const self = this;

let buffers: Buffer[] = [];
const buffers: Buffer[] = [];

for (const value of values) {
handleValue(value);
}

// This is a recursive function. It appends to the "buffers" variable.
function handleValue(value: TypedValue): void {
// TODO: Use matchers.

if (value.hasExactClass(OptionalValue.ClassName)) {
let valueAsOptional = <OptionalValue>value;
const valueAsOptional = <OptionalValue>value;

if (valueAsOptional.isSet()) {
handleValue(valueAsOptional.getTypedValue());
}
} else if (value.hasExactClass(VariadicValue.ClassName)) {
let valueAsVariadic = <VariadicValue>value;
for (const item of valueAsVariadic.getItems()) {
handleValue(item);
}
} else if (value.hasExactClass(CompositeValue.ClassName)) {
let valueAsComposite = <CompositeValue>value;

return;
}

if (value.hasExactClass(VariadicValue.ClassName)) {
handleVariadicValue(<VariadicValue>value);
return;
}

if (value.hasExactClass(CompositeValue.ClassName)) {
const valueAsComposite = <CompositeValue>value;

for (const item of valueAsComposite.getItems()) {
handleValue(item);
}
} else {
// Non-composite (singular), non-variadic (fixed) type.
// The only branching without a recursive call.
let buffer: Buffer = self.codec.encodeTopLevel(value);
buffers.push(buffer);

return
}

// Non-composite (singular), non-variadic (fixed) type.
// The only branching without a recursive call.
const buffer: Buffer = self.codec.encodeTopLevel(value);
buffers.push(buffer);
}

function handleVariadicValue(value: VariadicValue): void {
const variadicType = <VariadicType>value.getType();

if (variadicType.isCounted) {
const countValue = new U32Value(value.getItems().length);
buffers.push(self.codec.encodeTopLevel(countValue));
}

for (const item of value.getItems()) {
handleValue(item);
}
}

Expand Down
90 changes: 86 additions & 4 deletions src/smartcontracts/nativeSerializer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import BigNumber from "bignumber.js";
import { assert } from "chai";
import { Address } from "../address";
import { ErrInvalidArgument } from "../errors";
import { NativeSerializer } from "./nativeSerializer";
import { AbiRegistry, AddressType, AddressValue, BigUIntType, BooleanType, BooleanValue, CompositeType, CompositeValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, EnumType, ListType, NullType, OptionalType, OptionalValue, OptionType, OptionValue, TupleType, U32Type, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "./typesystem";
import { AbiRegistry, AddressType, AddressValue, BigUIntType, BooleanType, BooleanValue, CompositeType, CompositeValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, ListType, NullType, OptionalType, OptionalValue, OptionType, OptionValue, TupleType, TypePlaceholder, U32Type, U32Value, U64Type, U64Value, U8Type, U8Value, VariadicType, VariadicValue } from "./typesystem";
import { BytesType, BytesValue } from "./typesystem/bytes";

describe("test native serializer", () => {
Expand Down Expand Up @@ -45,6 +46,87 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[6].valueOf(), null);
});

it("should perform type inference (regular variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), false))];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);
const typedValues = NativeSerializer.nativeToTypedValues([8, 9, 10], endpoint);

assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), false));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
});

it("should perform type inference (counted-variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [
new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true)),
new EndpointParameterDefinition("", "", new VariadicType(new BytesType(), true))
];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);

// Implicit counted-variadic (not supported).
assert.throws(() => NativeSerializer.nativeToTypedValues([8, 9, 10, "a", "b", "c"], endpoint), ErrInvalidArgument);

// Explicit, non-empty counted-variadic.
let typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10)),
VariadicValue.fromItemsCounted(BytesValue.fromUTF8("a"), BytesValue.fromUTF8("b"), BytesValue.fromUTF8("c"))
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), true));
assert.deepEqual(typedValues[1].valueOf(), [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]);

// Explicit, empty counted-variadic.
typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(),
VariadicValue.fromItemsCounted()
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new TypePlaceholder(), true));
assert.deepEqual(typedValues[0].valueOf(), []);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new TypePlaceholder(), true));
assert.deepEqual(typedValues[1].valueOf(), []);
});

it("should perform type inference (counted-variadic and regular variadic arguments)", async () => {
const endpointModifiers = new EndpointModifiers("", []);
const inputParameters = [
new EndpointParameterDefinition("", "", new VariadicType(new U32Type(), true)),
new EndpointParameterDefinition("", "", new VariadicType(new BytesType(), false))
];
const endpoint = new EndpointDefinition("foo", inputParameters, [], endpointModifiers);

// Implicit counted-variadic (not supported).
assert.throws(() => NativeSerializer.nativeToTypedValues([8, 9, 10], endpoint), ErrInvalidArgument);

// Explicit counted-variadic, empty implicit regular variadic.
let typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10))
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), false));
assert.deepEqual(typedValues[1].valueOf(), []);

// Explicit counted-variadic, non-empty implicit regular variadic.
typedValues = NativeSerializer.nativeToTypedValues([
VariadicValue.fromItemsCounted(new U32Value(8), new U32Value(9), new U32Value(10)),
"a", "b", "c"
], endpoint);

assert.lengthOf(typedValues, 2);
assert.deepEqual(typedValues[0].getType(), new VariadicType(new U32Type(), true));
assert.deepEqual(typedValues[0].valueOf(), [new BigNumber(8), new BigNumber(9), new BigNumber(10)]);
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType(), false));
assert.deepEqual(typedValues[1].valueOf(), [Buffer.from("a"), Buffer.from("b"), Buffer.from("c")]);
});

it("should should handle optionals in a strict manner (but it does not)", async () => {
const endpoint = AbiRegistry.create({
"endpoints": [
Expand Down Expand Up @@ -279,7 +361,7 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[0].valueOf(), new BigNumber(42));
assert.deepEqual(typedValues[1].getType(), new VariadicType(new BytesType()));
assert.deepEqual(typedValues[1].valueOf(), []);
});
});

it("should perform type inference (enums)", async () => {
const abiRegistry = AbiRegistry.create({
Expand Down Expand Up @@ -354,8 +436,8 @@ describe("test native serializer", () => {
assert.deepEqual(typedValues[1].getType(), enumType);
assert.deepEqual(typedValues[1].valueOf(), { name: "Nothing", fields: [] });
assert.deepEqual(typedValues[2].getType(), enumType);
assert.deepEqual(typedValues[2].valueOf(), { name: 'Something', fields: [ new Address('erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha') ] });
assert.deepEqual(typedValues[2].valueOf(), { name: 'Something', fields: [new Address('erd1dc3yzxxeq69wvf583gw0h67td226gu2ahpk3k50qdgzzym8npltq7ndgha')] });
assert.deepEqual(typedValues[3].getType(), enumType);
assert.deepEqual(typedValues[3].valueOf(), { name: 'Else', fields: [ new BigNumber(42), new BigNumber(43) ] });
assert.deepEqual(typedValues[3].valueOf(), { name: 'Else', fields: [new BigNumber(42), new BigNumber(43)] });
});
});
57 changes: 39 additions & 18 deletions src/smartcontracts/nativeSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ export namespace NativeSerializer {
*/
export function nativeToTypedValues(args: any[], endpoint: EndpointDefinition): TypedValue[] {
args = args || [];
args = handleVariadicArgsAndRePack(args, endpoint);

checkArgumentsCardinality(args, endpoint);

if (hasNonCountedVariadicParameter(endpoint)) {
args = repackNonCountedVariadicParameters(args, endpoint);
} else {
// Repacking makes sense (it's possible) only for regular, non-counted variadic parameters.
}

let parameters = endpoint.input;
let values: TypedValue[] = [];
Expand All @@ -33,29 +40,39 @@ export namespace NativeSerializer {
return values;
}

function handleVariadicArgsAndRePack(args: any[], endpoint: EndpointDefinition) {
let parameters = endpoint.input;

let { min, max, variadic } = getArgumentsCardinality(parameters);
function checkArgumentsCardinality(args: any[], endpoint: EndpointDefinition) {
// With respect to the notes of "repackNonCountedVariadicParameters", "getArgumentsCardinality" will not be needed anymore.
// Currently, it is used only for a arguments count check, which will become redundant.
const { min, max } = getArgumentsCardinality(endpoint.input);

if (!(min <= args.length && args.length <= max)) {
throw new ErrInvalidArgument(`Wrong number of arguments for endpoint ${endpoint.name}: expected between ${min} and ${max} arguments, have ${args.length}`);
}
}

if (variadic) {
const lastEndpointParamIndex = parameters.length - 1;
const argAtIndex = args[lastEndpointParamIndex];

if (argAtIndex?.belongsToTypesystem) {
const isVariadicValue = argAtIndex.hasClassOrSuperclass(VariadicValue.ClassName);
if (!isVariadicValue) {
throw new ErrInvalidArgument(`Wrong argument type for endpoint ${endpoint.name}: typed value provided; expected variadic type, have ${argAtIndex.getClassName()}`);
}
function hasNonCountedVariadicParameter(endpoint: EndpointDefinition): boolean {
const lastParameter = endpoint.input[endpoint.input.length - 1];
return lastParameter?.type instanceof VariadicType && !lastParameter.type.isCounted;
}

// Do not repack.
} else {
args[lastEndpointParamIndex] = args.slice(lastEndpointParamIndex);
// In a future version of the type inference system, re-packing logic will be removed.
// The client code will be responsible for passing the correctly packed arguments (variadic arguments explicitly packed as arrays).
// For developers, calling `foo(["erd1", 42, [1, 2, 3]])` will be less ambiguous than `foo(["erd1", 42, 1, 2, 3])`.
// Furthermore, multiple counted-variadic arguments cannot be expressed in the current variant.
// E.g. now, it's unreasonable to decide that `foo([1, 2, 3, "a", "b", "c"])` calls `foo(counted-variadic<int>, counted-variadic<string>)`.
function repackNonCountedVariadicParameters(args: any[], endpoint: EndpointDefinition) {
const lastEndpointParamIndex = endpoint.input.length - 1;
const argAtIndex = args[lastEndpointParamIndex];

if (argAtIndex?.belongsToTypesystem) {
const isVariadicValue = argAtIndex.hasClassOrSuperclass(VariadicValue.ClassName);
if (!isVariadicValue) {
throw new ErrInvalidArgument(`Wrong argument type for endpoint ${endpoint.name}: typed value provided; expected variadic type, have ${argAtIndex.getClassName()}`);
}

// Do not repack.
} else {
args[lastEndpointParamIndex] = args.slice(lastEndpointParamIndex);
}

return args;
Expand Down Expand Up @@ -135,7 +152,11 @@ export namespace NativeSerializer {
return new OptionalValue(type, converted);
}

function toVariadicValue(native: any, type: Type, errorContext: ArgumentErrorContext): TypedValue {
function toVariadicValue(native: any, type: VariadicType, errorContext: ArgumentErrorContext): TypedValue {
if (type.isCounted) {
throw new ErrInvalidArgument(`Counted variadic arguments must be explicitly typed. E.g. use "VariadicValue.fromItemsCounted()" or "new VariadicValue()"`);
}

if (native == null) {
native = [];
}
Expand Down
Loading

0 comments on commit d8b4893

Please sign in to comment.