-
Notifications
You must be signed in to change notification settings - Fork 196
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
Meta: Language design #829
Comments
Thanks, @brockelmore for the kind words and the detailed comments! I'll try to respond to each point, but in summary, we mostly agree with you, and some of these things are on our collective mental backlog, but aren't yet logged as issues. We tend to discuss language direction either in person when we're able, or on our weekly video calls (open to everyone, btw, details on the discord meeting-topics channel), but of course much of this inevitably doesn't get documented.
There are some small syntax warts that are holdovers from the old days of python-like syntax (this project started as a rust implementation of vyper), eg we should allow commas in struct defs, struct initializers should use braces, etc. We just haven't gotten around to cleaning these things up yet. Semicolons, I've argued that we shouldn't add, as I think rust's use of semicolons isn't ideal, but we may add them if we can't avoid ambiguity etc in a satisfactory way without them. But, I think these small things feel weird because the functionality we've implemented so far feels like we're just building a rust-lite ("Shitty Rust", I call it) targeting the evm, but our plan is to diverge from rust in some significant ways. (I love rust as much as the next person, but some of its semantics and complexity make it an imperfect fit for this domain, IMO, and if one wants to use rust, it exists already). For example, the plan for contract syntax and semantics is to move to a message-passing style, which is a better fit of the actual underlying platform. I documented some of the ways in which the current struct-like contract formulation is misleading, and a rough sketch of what the message passing style might look like here: https://notes.ethereum.org/QVY5Uiz1SeCzj8KOuA9gtw?view (this document is a rough exploration of ideas, subject to change, should be polished, yada). If you have thoughts on this direction broadly or any of specifics, I'd love to hear them. It overlaps with some the stuff you mentioned above in the custom dispatch. Another tentative plan we have is to not follow rust's lead on explicit
No strong feelings here; the current plan is to allow
I like this general idea; I sorta touched on something similar in the message passing doc linked above, where contract storage is a particular struct type that implements a A currently open problem is to decide what sort of low-level primitives the standard library needs to provide to allow safe but powerful custom use of storage and memory.
👍 This should be a gh issue. We also plan to add an
Agreed on both points. I prefer Log/log() over Emittable/emit(). Much of this trait stuff is new, and the magical behavior of the Emittable trait is a temporary stop-gap solution.
#559 This feature is implemented but not documented yet, and lacks some supporting features that make it usable for normal contracts (like a way to easily match on calldata signature). See the message passing doc though.
The current hope is that we can provide sufficient power via unsafe std lib functions, but TBD. We plan to target more than just evm, via https://github.com/fe-lang/sonatina [I'm tired and out of steam for now; happy to discuss any of this further. I appreciate the time you took to braindump!] |
Oh, forgot to comment on the most important part:
Yes please! |
Wow thank you for the detailed response!
I am hesitant to advise the actor model (I spend a good amount of time with contract C {
pub fn foo() { .. }
fn g(self) {
let c: C = ..
c.foo() # OK
self.foo() # ERROR, `foo` does not take `self` use `foo()`
}
} should likely be: contract C {
pub fn foo() { .. }
pub fn bar(mut self) { .. }
fn g(self) {
let c = C(address(0));
self.foo() // this would error suggesting Self::foo()
Self::foo() // this would execute the function without a call
Self::foo(self) // this would error, and suggest C::foo(ctx.self_address()) if the user wants to make it a call or Self::foo() for internal
self.bar() // this would execute bar without a call as it takes a self
C::foo() // this would error as there is not a specified instance of C
C::foo(address(0)) // this would work because you provide it with an address
c.foo() // this would work because c is an instance of `C`
}
} This clarifies the internal vs external call by if you use
There seems to be a concept of Wow! So glad you guys are going down the path of not just Yul targeting, that makes me even more excited. Maybe one option would be do allow something like this: fn A(a: uint256) -> uint256 {
unsafe {
let asm_block = ctx.assembly(|ctx, elem| {
ctx.push(1)
ctx.add()
});
return asm_block(ctx, a)
}
} I understand if you are hesitant to do so, but I think there are a lot of the optimizors would love this feature. Do you guys have a chat some place? I may start hacking on some stuff and would love to bounce ideas around |
We could; we have unsafe functions in the std library (https://github.com/ethereum/fe/blob/master/crates/library/std/src/evm.fe) to perform these low-level operations, so we could probably do this approximately as you've written today, but the point I was trying to make is that it should be possible to make the Store trait implementable (in most cases at least) with only safe code, with the proper abstraction. Eg, something like:
Similarly for memory operations, your dynamic array code from memmove could probably be ported to fe as-is today, but it could instead be something like:
IMO, we should strive to eliminate all need for raw s/mloads and s/mstores, and the need to make assumptions about the memory allocation scheme (btw, fe currently copies solidity here, but that'll change sometime after the move to sonatina). Maybe we always need the escape hatch, but the less it's used the better. |
The point of that example is that the syntax exactly matches that of structs, but
The point I'm trying to make with that list of motivations is that "structlike" So what I'm suggesting is that instead of torturing the struct/function-call I don't think the result will necessarily feel like an using actor
Of course, there are issues to work out with this idea too, and the syntax above is just a first draft. Maybe it'll turn |
I like the MyMsg::Transfer { from: me, to: you, value: 10 }.send_to(impl Recv<MyMsg>) this feels better than using Then you could eventually do: if let Ok(resp: MyMsg::TransferResponse) = MyMsg::Transfer { from: me, to: you, value: 10 }.send_to(impl Recv<MyMsg>) {
// handle ok response
} else {
// handle failed, or use `?` to revert with a bubble revert
} A full example: #[derive(Msg)]
contract A {
pub fn a(mut self, a: u256, b: u256) -> u256 {
return a + b
}
}
// if we dont have the contract:
#[derive(Msg)]
abi A {
fn a(...)
}
contract B {
pub fn b(mut self, a: u256, b: u256) -> u256 {
// A is `impl Recv<A::Msg>`
let a_contract: A = address(0)
// A::Msg is `impl SendTo<T: Recv<Self>>`
let resp: u256 = A::Msg::a { a: a, b: b }.send_to(a_contract)?
return resp
}
}
// in std lib
pub trait Recv<T> {}
impl Recv<Msg> for A {}
// in std lib
pub trait Encodable {
fn encode(self) -> Vec<u8>;
}
impl Encodable for A::Msg { // snip }
// in std lib
pub trait SendTo: Encodable {
type Output;
fn send_to<T: Recv<Self>>(&self, ctx: Context, contract: T) -> Result<Self::Output, ()>;
}
impl SendTo for A::Msg {
type Output = A::MsgOutput;
fn send_to<T: Recv<Self>>(&self, ctx: Context, contract: T) -> Result<Self::Output, ()> {
ctx.call(self, contract)
}
}
impl Context {
pub fn call<V: Encodable, T: Recv<V>, Ret>(&self, data: V, contract: T) -> Result<Ret, ()>{
let bytes = data.encode();
// __call(contract, bytes);
}
// snip
} |
What is wrong?
Hi! Just checking out Fe for the first time really. As an avid rust user and solidity user, I like the direction Fe is headed!
However, I have a few gripes about language design (note, these are nitpicky! you guys have done a great job thus far!).
I could create separate issues for each of these but figured I would brain dump in one, and if any are deemed as worthwhile to pursue they can be separated out.
tl;dr: Trait all the things (and lean into rusty-ness)!
Storage
In my opinion, the ability to store something should be defined by if it implements a
Store
trait. Loading should be defined by aLoad
trait. For example:Which brings me on to the semantics of storage. Currently, they are just variables placed anywhere inside a
contract
keyword. In my mind something like this may be a better path forward:Any element that is in
storage
must implement theStore
andLoad
traits. The elements in here are automatically given storage slots from the compiler. (But any element that implementsStore
can still be stored manually by callingself.store(ctx, slot)
(orStore::store(self, ctx, slot)
if you cant resolve which version to use via the type system. Also lets library creators do cool things (This is something I needed for this: https://github.com/brockelmore/memmove)Events
As recent work has improved the event system, an ideal world would have an
Emittable
trait as has been worked on. But the current annotations of#indexed
feel weird. It is an unnecessary departure in my mind from rust macro syntax of#[indexed]
, which causes unnecessary overhead in the programmers mind, if coming from rust. Ideally we would reach a world where the following is possible:(even if there is a default implementation for all types and structs of
Emittable
, (btw trait naming should probably just beLog
orEmit
imo) it should be implementable manually)Mapping aside
If you have a
Hash
orMapping
trait, you can allow the user to define how to hash the struct used in aMap
, where theK: Hash
implementation is left to the user (or derived).Like rust, but not
That brings me to a broader point: there are a ton of really good semantics in this language - but my largest complaint is that there are a good number of things that are rust-like, but differ in an not meaningful way which just boils down to overhead for anyone coming from rust (lack of
;
, inability to use,
in structs (the combination of each leads to significant whitespace which sucks)).Function dispatch
A common request from solidity land is to be able to define the dispatch table (i.e. I know
transfer
will be called more thanmint
). One way that this could be achieved is as follows:The above has a few ideas mixed in there, (like treating functions as enum variants), a
main
function that is entered on every call, ability to set up the match as you want, etc.Abi as a kind of alias for
Trait
This is unabashedly borrowed from the
sway
lang (albeit, I dislike how they do it and suggested a similar thing to them to no avail). The idea is to make ABIs a first class citizen and to promote usage and implementation of them (could foresee a common set being in the std lib).Verbatim/Assembly block
Allow for usage of raw assembly blocks, a la
verbatim_i1_o1
style yul blocks. In an ideal world this would look like:In a perfect world, some analysis would be ran on the assembly block to ensure it keeps stack consistent with promised input/output (see my comment here: ethereum/solidity#12067 (comment) for more promises the user can make to the compiler about this code and when the compiler should emit an error/warning)
Anyways, sorry for the braindump here! I've enjoyed playing with Fe and hope to see continued work and improvement on it! I have started looking through the codebase and if there is alignment on any of the above I can try to hop in an help (I am a core contributor to
foundry
, helping withreth
, and have chatted with the solidity team a bunch about optimizations and such).Thanks for y'alls work!
The text was updated successfully, but these errors were encountered: