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 a Load
trait. For example:
struct Reserves {
pub reserve0: u128
pub reserve1: u128
}
impl std::storage::Store for Reserves {
fn store(self, mut ctx: Context, slot: bytes32) {
// more on slot derivation later
let packed = self.reserve0 << 128 | self.reserve1;
ctx.store(slot, packed);
}
}
impl std::storage::Load for Reserves {
fn load(ctx: Context, slot: bytes32) -> Self {
// more on slot derivation later
let val = ctx.load(slot);
return Sync {
reserve0: val >> 128,
reserve1: val ^ (((1 << 128) - 1) << 128),
}
}
}
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:
contract MyContract {
/// Contract storage
storage {
a: A
b: u256
// You can have auto-generated view functions based on pub keyword in storage
pub c: u128
}
}
Any element that is in storage
must implement the Store
and Load
traits. The elements in here are automatically given storage slots from the compiler. (But any element that implements Store
can still be stored manually by calling self.store(ctx, slot)
(or Store::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:
// additionally Emittable maybe not the best word for this? i like Log
#[derive(Emittable)]
struct AuctionEnded {
#[indexed]
pub winner: address
pub amount: u256
}
// alternatively manually implement it:
struct A {
a: u256
b: u128
}
/// Implements how to log struct A, could be derived via "Log"
impl std::log::Log for A {
fn log(self) {
std::log::log4(a.a, a.b, a.c, a.d);
// could have alternatively done:
// std::log::log3(a.a, a.b, a.c, a.d);
// which would do log3 op instead. The derive macro would
// match and order appropriately
}
}
(even if there is a default implementation for all types and structs of Emittable
, (btw trait naming should probably just be Log
or Emit
imo) it should be implementable manually)
Mapping aside
If you have a Hash
or Mapping
trait, you can allow the user to define how to hash the struct used in a Map
, where the K: 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 than mint
). One way that this could be achieved is as follows:
contract SimpleOpenAuction {
pub fn main(self, mut ctx: Context) {
match ctx.signature() {
// main can be unconstrained in return type, or be `-> bytes` maybe?
Self::A => { return self.A() },
Self::B => { self.B(ctx) },
}
}
pub fn A(self) -> u256 {
// do something
return 1
}
pub fn B(self, ctx: Context) {
// do something else
}
}
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).
// abi keyword, similar to a trait, but for contracts
abi ERC2612 {
pub fn permit(owner: address, spender: address, value: u256, deadline: u256, v: u8, r: bytes32, s: bytes32);
pub fn nonces(owner: address) -> u256;
pub fn DOMAIN_SEPARATOR() -> bytes32;
}
contract MyContract {
// A contract can impl an abi trait
// not sure if this should be *inside* the contract span or be implementable
// outside like `impl ERC2612 for MyContract {..}`
impl ERC2612 for Self {
pub fn permit(owner: address, spender: address, value: u256, deadline: u256, v: u8, r: bytes32, s: bytes32) {
// snip
}
pub fn nonces(owner: address) -> u256 {
0
}
pub fn DOMAIN_SEPARATOR() -> bytes32 {
bytes32::new(0)
}
}
}
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:
// asm is a keyword similar to rust's async but instead generates an assembly block
asm fn my_assembly_fn(a: uint256) -> u256 {
PUSH 0x11
ADD
}
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: https://github.com/ethereum/solidity/issues/12067#issuecomment-1270388088 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 with reth
, and have chatted with the solidity team a bunch about optimizations and such).
Thanks for y'alls work!