Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions bin/edgec/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,8 @@ fn main() -> Result<()> {

if let Some(level) = level {
use tracing_subscriber::EnvFilter;
// Egglog is extremely noisy, suppress it unless TRACE level
let egglog_level = if level >= Level::TRACE {
"trace"
} else {
"warn"
};
// Egglog is extremely noisy — only enable at verbosity 5+ (-vvvvv)
let egglog_level = if cli.verbose >= 5 { "trace" } else { "warn" };
let filter = format!("edge={level},egglog={egglog_level},{level}");
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new(filter))
Expand Down
197 changes: 193 additions & 4 deletions crates/driver/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ impl Compiler {
.render_to_string(&path, &self.session.source)
}

/// Parse and resolve imports, returning the preprocessed AST.
/// Useful for tests that need to control IR/codegen optimization levels separately.
pub fn parse_and_resolve(&mut self) -> Result<Program, CompileError> {
let _tokens = self.lex()?;
let mut ast = self.parse()?;
self.resolve_imports(&mut ast)?;
Ok(ast)
}

/// Run the compilation pipeline
pub fn compile(&mut self) -> Result<CompileOutput, CompileError> {
tracing::info!("Compiling {:?}", self.session.config.input_file);
Expand Down Expand Up @@ -356,22 +365,51 @@ impl Compiler {
/// Run the parser and produce an AST
fn parse(&mut self) -> Result<Program, CompileError> {
let mut parser = Parser::new(&self.session.source).map_err(|e| {
self.session
.emit_error(Diagnostic::error(format!("parse error: {e}")));
self.session.emit_error(Self::parse_error_to_diagnostic(&e));
CompileError::ParseErrors
})?;

match parser.parse() {
Ok(program) => Ok(program),
Err(e) => {
self.session
.emit_error(Diagnostic::error(format!("parse error: {e}")));
self.session.emit_error(Self::parse_error_to_diagnostic(&e));
self.session.report_diagnostics();
Err(CompileError::ParseErrors)
}
}
}

/// Convert a `ParseError` into a `Diagnostic` with proper span labels.
fn parse_error_to_diagnostic(e: &edge_parser::ParseError) -> Diagnostic {
use edge_parser::ParseError;
match e {
ParseError::UnexpectedToken {
found,
expected,
span,
} => Diagnostic::error(format!("expected {expected}, found {found}"))
.with_label(span.clone(), format!("expected {expected}")),
ParseError::InvalidTypeSig { message, span } => {
Diagnostic::error(format!("invalid type: {message}"))
.with_label(span.clone(), message.clone())
}
ParseError::InvalidExpr { message, span } => {
Diagnostic::error(format!("invalid expression: {message}"))
.with_label(span.clone(), message.clone())
}
ParseError::InvalidStmt { message, span } => {
Diagnostic::error(format!("invalid statement: {message}"))
.with_label(span.clone(), message.clone())
}
ParseError::InvalidPattern { message, span } => {
Diagnostic::error(format!("invalid pattern: {message}"))
.with_label(span.clone(), message.clone())
}
ParseError::UnexpectedEof => Diagnostic::error("unexpected end of file"),
ParseError::LexerError(msg) => Diagnostic::error(format!("lexer error: {msg}")),
}
}

/// Resolve `use std::...` imports by locating source for each imported module —
/// first from an explicit filesystem override, then from the stdlib embedded in
/// the binary — and merging their top-level items into the program AST.
Expand All @@ -382,6 +420,10 @@ impl Compiler {
/// 2. Embedded sources baked into the binary at compile time via `build.rs`
/// (works on any machine with no extra setup).
fn resolve_imports(&mut self, ast: &mut Program) -> Result<(), CompileError> {
// Auto-import globals: ops, map, option, result.
// These are always available without explicit `use` statements.
self.auto_import_globals(ast)?;

// Collect std imports from the AST.
// Build a full path-segments list by combining intermediate `segments` with the final
// `path` identifier. For example:
Expand Down Expand Up @@ -516,6 +558,106 @@ impl Compiler {
Ok(())
}

/// Auto-import all `std/globals/*.edge` files — these are always available
/// without explicit `use` statements. Prepends their declarations (types,
/// traits, impls, functions) to the AST.
fn auto_import_globals(&mut self, ast: &mut Program) -> Result<(), CompileError> {
// Order matters: ops first (trait defs), then map (uses ops traits).
let global_keys = [
"globals/ops",
"globals/option",
"globals/result",
"globals/map",
];
let mut new_stmts: Vec<edge_ast::Stmt> = Vec::new();

// Canonicalize the explicit override path once (if provided).
let explicit_std_path: Option<std::path::PathBuf> =
self.session.config.std_path.as_ref().and_then(|p| {
let canon = std::fs::canonicalize(p).unwrap_or_else(|_| p.clone());
if canon.is_dir() {
Some(canon)
} else {
None
}
});

for key in &global_keys {
let segments: Vec<String> = key.split('/').map(String::from).collect();
let source = explicit_std_path.as_ref().map_or_else(
|| Self::try_read_from_embedded(&segments).map(String::from),
|std_path| {
Self::try_read_from_fs(std_path, &segments)
.or_else(|| Self::try_read_from_embedded(&segments).map(String::from))
},
);

let Some(source) = source else {
// Globals not available (e.g., downstream consumer without std/).
continue;
};

let mut parser = Parser::new(&source).map_err(|e| {
self.session.emit_error(Diagnostic::error(format!(
"parse error in globals `{key}`: {e}"
)));
CompileError::ParseErrors
})?;

let program = parser.parse().map_err(|e| {
self.session.emit_error(Diagnostic::error(format!(
"parse error in globals `{key}`: {e}"
)));
self.session.report_diagnostics();
CompileError::ParseErrors
})?;

// Include everything except ModuleImport/ModuleDecl (internal imports).
for stmt in program.stmts {
if !matches!(
stmt,
edge_ast::Stmt::ModuleImport(_) | edge_ast::Stmt::ModuleDecl(_)
) {
new_stmts.push(stmt);
}
}
}

if !new_stmts.is_empty() {
// Collect names defined in the user's file so globals don't shadow them.
// Like Rust's prelude: local definitions take priority over auto-imports.
let user_defined: std::collections::HashSet<String> = ast
.stmts
.iter()
.filter_map(|stmt| match stmt {
edge_ast::Stmt::TypeAssign(td, _, _) => Some(td.name.name.clone()),
edge_ast::Stmt::TraitDecl(tr, _) => Some(tr.name.name.clone()),
edge_ast::Stmt::FnAssign(fd, _) | edge_ast::Stmt::ComptimeFn(fd, _) => {
Some(fd.name.name.clone())
}
_ => None,
})
.collect();

// Filter out global statements whose name collides with user definitions.
new_stmts.retain(|stmt| {
let name = match stmt {
edge_ast::Stmt::TypeAssign(td, _, _) => Some(&td.name.name),
edge_ast::Stmt::TraitDecl(tr, _) => Some(&tr.name.name),
edge_ast::Stmt::FnAssign(fd, _) | edge_ast::Stmt::ComptimeFn(fd, _) => {
Some(&fd.name.name)
}
_ => None,
};
name.is_none_or(|n| !user_defined.contains(n))
});

new_stmts.append(&mut ast.stmts);
ast.stmts = new_stmts;
}
Ok(())
}

/// Resolve a set of import path segments to a `(module_key, source)` pair.
///
/// Tries, in order:
Expand Down Expand Up @@ -588,6 +730,53 @@ impl Compiler {
}
}

// Before giving up, check if this import points to a globals module
// (e.g., `use std::ops::Add` → file "ops" not found, but "globals/ops" exists).
// If so, the content is already auto-imported — just return it.
//
// Try two forms:
// 1. Full path: ["globals"] + segments (e.g., "globals/ops/Add")
// 2. Symbol-level: ["globals"] + segments[..n-1], symbol = segments[n-1]
// (e.g., "globals/ops" with symbol "Add")
{
// Form 1: full path with globals prefix
let fallback_segments: Vec<String> = std::iter::once("globals".to_string())
.chain(segments.iter().cloned())
.collect();

if let Some(ref std_path) = explicit_std_path {
if let Some(source) = Self::try_read_from_fs(std_path, &fallback_segments) {
let key = fallback_segments.join("/");
return Ok((key, source, None));
}
}

if let Some(source) = Self::try_read_from_embedded(&fallback_segments) {
let key = fallback_segments.join("/");
return Ok((key, source.to_string(), None));
}

// Form 2: strip last segment as symbol name within globals file
if segments.len() > 1 {
let symbol = segments.last().unwrap().clone();
let file_fallback: Vec<String> = std::iter::once("globals".to_string())
.chain(segments[..segments.len() - 1].iter().cloned())
.collect();

if let Some(ref std_path) = explicit_std_path {
if let Some(source) = Self::try_read_from_fs(std_path, &file_fallback) {
let key = file_fallback.join("/");
return Ok((key, source, Some(symbol)));
}
}

if let Some(source) = Self::try_read_from_embedded(&file_fallback) {
let key = file_fallback.join("/");
return Ok((key, source.to_string(), Some(symbol)));
}
}
}

// Nothing found — emit a helpful error.
let module_path = segments.join("::");
self.session.emit_error(Diagnostic::error(format!(
Expand Down
12 changes: 6 additions & 6 deletions crates/e2e/.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ test_loop_storage::get_total(), 2255, 2270, 2270, 2270
test_loop_storage::read_write_loop(uint256), 44981, 44996, 44996, 44996
test_loop_storage::reset(), 4757, 4775, 4775, 4775
test_mappings::counter_get(address), 2287, 2302, 2302, 2302
test_mappings::counter_inc(address), 22446, 22453, 22453, 22453
test_mappings::map_add(address,uint256), 5346, 5350, 5350, 5350
test_mappings::counter_inc(address), 22449, 22456, 22456, 22456
test_mappings::map_add(address,uint256), 5349, 5353, 5353, 5353
test_mappings::map_get(address), 2235, 2250, 2250, 2250
test_mappings::map_set(address,uint256), 22332, 22345, 22345, 22345
test_mappings::map_set(address,uint256), 22335, 22348, 22348, 22348
test_mappings::nested_get(address,address), 2449, 2455, 2455, 2455
test_mappings::nested_set(address,address,uint256), 22444, 22448, 22448, 22448
test_mappings::nested_two_spenders(address,address,address,uint256,uint256), 44693, 44679, 44679, 44679
test_mappings::two_keys(address,address,uint256,uint256), 44473, 44474, 44474, 44474
test_mappings::nested_set(address,address,uint256), 22447, 22451, 22451, 22451
test_mappings::nested_two_spenders(address,address,address,uint256,uint256), 44699, 44685, 44685, 44685
test_mappings::two_keys(address,address,uint256,uint256), 44479, 44480, 44480, 44480
test_merkle::hash_two(bytes32,bytes32), 516, 516, 516, 516
test_merkle::verify(bytes32,bytes32,bytes32[4],uint256), 1530, 1530, 1530, 1530
test_packed_storage::store_and_read_b(), 22327, 22289, 22273, 22273
Expand Down
3 changes: 3 additions & 0 deletions crates/e2e/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ mod utils_exec;
#[path = "suites/warnings.rs"]
mod warnings;

#[path = "suites/map_std_exec.rs"]
mod map_std_exec;

#[path = "suites/int_widths_exec.rs"]
mod int_widths_exec;
#[path = "suites/large_int_literals.rs"]
Expand Down
Loading
Loading