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

Allow construction of new objects #981

Open
aslesarenko opened this issue May 13, 2024 · 19 comments
Open

Allow construction of new objects #981

aslesarenko opened this issue May 13, 2024 · 19 comments
Assignees
Labels
A-frontend Area: ErgoScript compiler (source -> ErgoTree) C-feature Category: Feature request or PR
Milestone

Comments

@aslesarenko
Copy link
Member

Problem

Currently there is no way in ErgoScript to create objects of pre-defined types such as AvlTree, Header, Box, etc. It is possible to introduce new objects only by deserializing from context var or from register.
But this is very limited approach and more like a workaround.

Solution

  • Introduce generic NewObject[T <: SType](args: Seq[SValue], tpe: T) extends Value[T] operation to ErgoTree which constructs a new object from args by calling the default constructor.
  • This operation will be similar to PolyMethodCall operation, except it will not have the receiver object.
  • In ErgoScript this can look like val tree = AvlTree(digest, flags, keyLength, valueLength) i.e. the same way constructors are called in Scala3 by using type name.
  • To compile ErgoScript to NewObject, the constructors can be declared in metadata similar to how SMethods are declared
  • Use cases: AvlTree, Header, Box, BigInt, GroupElement, Some, None, etc.
@aslesarenko aslesarenko added soft-fork Implementation requires soft-fork A-consensus Area: Code used in consensus (i.e. transaction validation) C-feature Category: Feature request or PR labels May 13, 2024
@aslesarenko aslesarenko added this to the v6.0 milestone May 13, 2024
@aslesarenko aslesarenko self-assigned this May 13, 2024
@kushti
Copy link
Member

kushti commented May 13, 2024

What is the motivation for those constructors? Any example ? Why BigInt is in the list?

@kushti
Copy link
Member

kushti commented May 13, 2024

You already can do bigInt("")

For compile-time creation of objects of (AvlTree, Header, Box, BigInt, GroupElement) you can use deserializeRaw from hardcoded bytes. Creating header from fields in runtime ? Any useful example ?

@kushti kushti removed this from the v6.0 milestone May 13, 2024
@aslesarenko
Copy link
Member Author

What is the motivation for those constructors? Any example ? Why BigInt is in the list?

The motivation is to have single ErgoTree encoding for different use cases.
This is actually pretty standard way of encoding new ObjectType(...) operations on languages.
We could have it from network launch along with MethodCall, but we didn't have time for this.

CreateAvlTree is one example. Another is val h = Header(...), as well as basically any other type.

BigInt maybe not the best example, as there is already byteArrayToBigInt method, but why not.
If any type can have a constructor, why not BigInt?

@aslesarenko
Copy link
Member Author

You already can do bigInt("")

This only allow constant argument, where as NewObject will allow any expressions in arguments.

@aslesarenko
Copy link
Member Author

aslesarenko commented May 13, 2024

For compile-time creation of objects of (AvlTree, Header, Box, BigInt, GroupElement) you can use deserializeRaw from hardcoded bytes.

yes, key words are hardcoded bytes, this may be both advantage and disadvantage, depending on use case.

Creating header from fields in runtime ? Any useful example ?

  • When an object is constructed explicitly with constructor, some arguments can come from template parameters, while others can be embedded as constants of expressions.
  • Thus you can construct header (or any other type) from different sources: embedded constants, template parameters, registers, context vars, lambda arguments.

@kushti
Copy link
Member

kushti commented May 13, 2024

I guess these constructors can be done on the compiler level anyway, so under the hood the compiler will replace a constructor with expression which is building a bytestring and then do deserializeRaw or deserializeTyped for it

Thus I am moving it to 6.x

@kushti kushti added this to the v6.x milestone May 13, 2024
@kushti kushti added A-frontend Area: ErgoScript compiler (source -> ErgoTree) and removed soft-fork Implementation requires soft-fork A-consensus Area: Code used in consensus (i.e. transaction validation) labels May 13, 2024
@aslesarenko
Copy link
Member Author

expression which is building a bytestring

So, of some arguments come from GetVar, then they should be serialized and then concatenated to the rest of the bytes? And this will happen during contract execution?
Isn't it too much efforts to avoid having normal operation?

@kushti
Copy link
Member

kushti commented May 13, 2024

It depends on usage, I haven't seen any use-case still. Especially considering that in practice you can go the other way, so instead of constructing e.g. a box with certain fields, expect for a box being provided and then calculate predicates over its fields (which is what most of apps are doing btw)

@aslesarenko
Copy link
Member Author

I haven't seen any use-case still.

I described the usecase where parameters can come from different sources. And the most natural way is to have constructor call.
Any work around looks ugly at use, and maybe more difficult in implementation.
The only thing we save here is time spent on v6.0.
We can move this to v7.0, but implementing it via pure compilation+serialization is even more complex.

@kushti
Copy link
Member

kushti commented May 13, 2024

But where do you need for constructing e.g. box from multiple sources instead of checking a box of interest ?

Even smaller chance to find an use case for a header.

@Luivatra
Copy link

I have wanted to make a reusable function with an option as parameter, which works fine when feeding it the result of an option producing function, but sometimes it is an actual value and creating a Some out of that would be nice.

@aslesarenko
Copy link
Member Author

Even smaller chance to find an use case for a header.

cmon, what kind of argument it is? Users found use cases for almost all language features I've put into the language.
Even Box in a register, which was added to DataSerializer just because it was easy to do and because I wanted to have generic serializer which works for any type with descriptor, and Box was such type.

The same is here, NewObject is generic feature which will work for any type.
I'm pretty sure people will find use cases.

@kushti
Copy link
Member

kushti commented May 13, 2024

As no use-cases provided, I can move it to 7.0 or leave for 6.x. Frontend only implementation looks promising, as it is better not to touch consensus-critical level when possible

@aslesarenko
Copy link
Member Author

As no use-cases provided

No use case for Header, but what about AvlTree, which we started from.
Why lack of usecase for Header is so critical, what about other types?

@kushti
Copy link
Member

kushti commented May 13, 2024

For AvlTree byte machinery is not hard:

val digest = SELF.R4[Coll[Byte]].get
val flag = Coll(0.toByte) // read-only
val keyLength = Coll(32.toByte)
val valueLength = Coll(0.toByte) // arb length

val treeBytes = digest ++ flag ++ keyLength ++ valueLength
deserialize[AvlTree](treeBytes).digest == digest

so here you have fields coming from different sources etc

the example is artificial though, as in practice (keyLength, valueLength) are coming from the same source as digest

@aslesarenko
Copy link
Member Author

(keyLength, valueLength) are coming from the same source as digest

To your point, if they come say from a register as AvlTree, then

@contract MyContract(flags: Byte) = {
  val tree = SELF.R4[AvlTree].get
  AvlTree(tree.digest, flags, tree.keyLength, tree.valueLength).digest == digest
}

The this examples not only much easier to understand/read, but also always works, while your code is only good while values fit into single bytes, user need to take care about format, and also know how to encode.

@kushti
Copy link
Member

kushti commented May 13, 2024

But there is simpler solution:

@contract MyContract(flags: Byte) = {
  val tree = SELF.R4[AvlTree].get
  tree.updateOperations(flags).digest == digest
}

@aslesarenko
Copy link
Member Author

But there is simpler solution:

Yes, that is because AvlTree have only fields.
In case you compare not digest but some computable property or the result of a method call, then you have to construct an object.

@kushti
Copy link
Member

kushti commented May 13, 2024

Yes, that is because AvlTree have only fields. In case you compare not digest but some computable property or the result of a method call, then you have to construct an object.

updateOperations(flags) is constructing an object, and then you can call any tree method on it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-frontend Area: ErgoScript compiler (source -> ErgoTree) C-feature Category: Feature request or PR
Projects
None yet
Development

No branches or pull requests

3 participants