Skip to content

feat: stdlib improvements, trait dispatch on primitives, generics soundness#71

Merged
refcell merged 5 commits into
mainfrom
brock/std_lib_improvements
Mar 16, 2026
Merged

feat: stdlib improvements, trait dispatch on primitives, generics soundness#71
refcell merged 5 commits into
mainfrom
brock/std_lib_improvements

Conversation

@brockelmore
Copy link
Copy Markdown
Collaborator

@brockelmore brockelmore commented Mar 11, 2026

Summary

  • Trait method calls on primitives: u256.unsafe_add(b) style calls now work via trait impl lookup with span-based error messages
  • Generic Map with Index trait dispatch: Replaced hardcoded starts_with("Map") checks with proper Index trait impl lookup — any user-defined type implementing Index gets the same treatment
  • Generics soundness fixes: resolve_generic_type_name() now returns None for ambiguous monomorphizations instead of silently picking the first; added type_sig_hint threading and resolve_generic_type_name_with_args() for precise resolution
  • is_primitive_type() fix: Validates width ranges matching lexer rules (u/i 8-256 step 8, bytes 1-32) instead of matching any prefix
  • Stdlib auto-imports: Map, ops, Option, Result auto-imported as globals
  • Refactored calls.rs: Extracted helpers, removed StructParamInfo
  • Clippy clean + nightly fmt

Adds a few stdlib traits:

// Index trait allows for operator overloading of indexing, i.e. my_map[a][b] or my_array[0]
// using a custom index and output.
trait Index<Idx, Output> {
    fn index(self, index: Idx) -> (Output);
}

trait UniqueSlot {
    // Derive a storage slot from this key and a base slot.
    // EVM convention: keccak256(abi.encode(key, base_slot))
    // Compiler provides implementations for primitive types.
    fn derive_slot(self, base_slot: u256) -> u256;
}

trait Sstore {
    fn sstore(self, base_slot: u256);
}

trait Sload {
    fn sload(base_slot: u256) -> Self;
}

Index allows for operator overloading of [] syntax. UniqueSlot is a way to customize slot calculation - takes a base slot provided by the compiler. Introduce Sstore + Sload traits which tell the compiler what to do for loading and storing to storage. Here is an example that is in the examples/tests/test_map_std.edge:

use std::ops::UniqueSlot;
use std::ops::Sstore;
use std::ops::Sload;

type CustomSStore = {
    ignored: u256,
    packed_a: u128,
    packed_b: u128
};

type CustomHash = {
    a: u128,
    b: u128
};

// Struct key with NO UniqueSlot impl — uses default keccak-chained derive_slot
type DefaultKey = {
    x: u256,
    y: u256
};

impl CustomHash: UniqueSlot {
    fn derive_slot(self, base_slot: u256) -> u256 {
        let packed_combo: u256 = (self.a << 128) | self.b;
        base_slot + packed_combo
    }
}

impl CustomSStore: Sstore {
    fn sstore(self, base_slot: u256) {
        let packed_combo: u256 = (self.packed_a << 128) | self.packed_b;
        packed_combo.sstore(base_slot);
    }
}

impl CustomSStore: Sload {
    fn sload(base_slot: u256) -> Self {
        let packed_combo = u256::sload(base_slot);
        let a = packed_combo >> 128;
        let b = packed_combo & ((1 << 128) - 1);
        CustomSStore { ignored: 0, packed_a: a, packed_b: b }
    }
}

contract TestMappings {
    let basic_map: &s Map<u256, u256>;
    let custom: &s CustomSStore;
    let custom_sstore_map: &s Map<u256, CustomSStore>;
    let double_custom_sstore_map: &s Map<CustomHash, CustomSStore>;
    let default_key_map: &s Map<DefaultKey, u256>;

    // Getters
    pub fn get_custom() -> CustomSStore {
        custom
    }

    pub fn get_basic(key: u256) -> u256 {
        basic_map.get(key)
    }

    pub fn get_basic_by_indexable(key: u256) -> u256 {
        basic_map[key]
    }

    pub fn get_custom(key: u256) -> u256 {
        let val: CustomSStore = custom_sstore_map.get(key);
        (val.packed_a << 128) | val.packed_b
    }

    pub fn get_custom_by_indexable(key: u256) -> u256 {
        let val: CustomSStore = custom_sstore_map[key];
        (val.packed_a << 128) | val.packed_b
    }

    // Setters
    pub mut fn set_custom(a: u128, b: u128) {
        custom = CustomSStore { ignored: 10000, packed_a: a, packed_b: b }
    }

    pub mut fn set_basic(key: u256, val: u256) -> u256 {
        basic_map.set(key, val);
    }

    pub mut fn set_basic_by_indexable(key: u256, val: u256) -> u256 {
        basic_map[key] = val;
    }

    pub mut fn set_custom(key: u256, val: CustomSStore) {
        custom_sstore_map.set(key, val);
    }

    pub mut fn set_custom_by_indexable(key: u256, val: CustomSStore) {
        custom_sstore_map[key] = val;
    }

    // Double custom: CustomHash key + CustomSStore value
    pub fn get_double_custom(a: u128, b: u128) -> u256 {
        let key = CustomHash { a: a, b: b };
        let val: CustomSStore = double_custom_sstore_map.get(key);
        (val.packed_a << 128) | val.packed_b
    }

    pub mut fn set_double_custom(a: u128, b: u128, val_a: u128, val_b: u128) {
        let key = CustomHash { a: a, b: b };
        let val = CustomSStore { ignored: 0, packed_a: val_a, packed_b: val_b };
        double_custom_sstore_map.set(key, val);
    }

    // Default derive_slot (no UniqueSlot impl on DefaultKey)
    pub fn get_default_key(x: u256, y: u256) -> u256 {
        let key = DefaultKey { x: x, y: y };
        default_key_map.get(key)
    }

    pub mut fn set_default_key(x: u256, y: u256, val: u256) {
        let key = DefaultKey { x: x, y: y };
        default_key_map.set(key, val);
    }
}

Notably: this removes map as a primitive and instead just represents it using the normal type system and gives users the flexibility to really get the most out of their contract.

brockelmore and others added 5 commits March 10, 2026 20:32
…inue warnings

Enable dot-syntax for compiler-provided trait methods on primitive types
(e.g., `a.unsafe_add(b)` instead of `UnsafeAdd::unsafe_add(a, b)`).
Supports chained calls and complex receiver expressions.

Convert all `IrError::Lowering(String)` to span-based `IrError::Diagnostic`
for better error messages with source locations. Remove the now-unused
`Lowering` variant. Emit compiler warnings for unimplemented `break`/`continue`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…port globals

- Fix monomorphization cache collision: Map<u256, u256> and Map<CustomHash,
  CustomSStore> no longer share cache entries (use mangled type names instead
  of EvmType for cache keys)
- Fix composite_base propagation: struct params inferred from type sigs during
  inlining now get composite_base set to their value (enables field access in
  trait method bodies)
- Add struct param memory allocation for calldata-passed struct params in
  contract functions (CALLDATACOPY to allocated memory region)
- Add default keccak-chained derive_slot for struct types without explicit
  UniqueSlot impl (Solidity nested mapping convention)
- Auto-import std/globals (ops, map, option, result) without explicit use
- Move std/ops.edge to std/globals/ops.edge, add globals/map.edge
- Add build_type_param_subst with base name fallback for mangled generics
- Add tracing at trace level for method dispatch and inline param binding
- Egglog tracing now requires verbosity 5 (-vvvvv) instead of 4
- Add Map<CustomHash, CustomSStore> and Map<DefaultKey, u256> e2e tests
  (21 map_std tests total)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Extract `lookup_binding_for_expr()` to deduplicate scope traversal
  in `infer_receiver_type` and `infer_receiver_type_args`
- Extract `try_compiler_stateful_dispatch()` to deduplicate the
  lower-receiver + lower-args + compiler_provided_stateful_method pattern
- Replace `StructParamInfo` struct with simple `(String, usize)` tuple —
  callsite only needs name and field count, no need to clone fields

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…enerics soundness

- Replace starts_with("Map") checks in infer_receiver_type, infer_receiver_type_args,
  and inline_function_call with Index trait impl Output type lookup
- Add trait_type_args to TraitImplInfo and GenericImplBlock for type-system-based dispatch
- Fix is_primitive_type to validate width ranges (8..=256 step 8) matching lexer rules
- Fix resolve_generic_type_name to return None on ambiguous multiple monomorphizations
- Add resolve_generic_type_name_with_args for precise resolution with type context
- Add type_sig_hint threading from VarDecl to struct instantiation for disambiguation
- Fix composite_type_args propagation in inline_function_call (was dropping to Vec::new)
- Improve error messages: "ambiguous generic type" instead of "unknown" for multi-monomorph

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 11, 2026

Deploy Preview for edgelang ready!

Name Link
🔨 Latest commit c5e1238
🔍 Latest deploy log https://app.netlify.com/projects/edgelang/deploys/69b1d4b66f9d0f0008e954dd
😎 Deploy Preview https://deploy-preview-71--edgelang.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown

Gas Snapshot Diff

Changed (6) | Unchanged (137)

Test O0 O1 O2 O3
test_mappings::counter_inc(address) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%)
test_mappings::nested_set(address,address,uint256) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%)
test_mappings::map_set(address,uint256) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%) +3 (+0.0%)
test_mappings::nested_two_spenders(address,address,address,uint256,uint256) +6 (+0.0%) +6 (+0.0%) +6 (+0.0%) +6 (+0.0%)
test_mappings::two_keys(address,address,uint256,uint256) +6 (+0.0%) +6 (+0.0%) +6 (+0.0%) +6 (+0.0%)
test_mappings::map_add(address,uint256) +3 (+0.1%) +3 (+0.1%) +3 (+0.1%) +3 (+0.1%)
TOTAL +24 (+0.0%) +24 (+0.0%) +24 (+0.0%) +24 (+0.0%)

Regressions at O3 (6)

Test O3
test_mappings::counter_inc(address) +3 (+0.0%)
test_mappings::nested_set(address,address,uint256) +3 (+0.0%)
test_mappings::map_set(address,uint256) +3 (+0.0%)
test_mappings::nested_two_spenders(address,address,address,uint256,uint256) +6 (+0.0%)
test_mappings::two_keys(address,address,uint256,uint256) +6 (+0.0%)
test_mappings::map_add(address,uint256) +3 (+0.1%)

@refcell refcell merged commit c5e1238 into main Mar 16, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants