Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 18 additions & 13 deletions crates/compiler/benchmarks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,17 @@ fn parse_interface_stub(program_name: Symbol, source: &Path, source_dir: &Path)
.collect::<Vec<_>>();
let node_builder = NodeBuilder::default();

let program =
match parse_program(Handler::default(), &node_builder, &source_file, &module_source_files, BENCH_NETWORK) {
Ok(ast) => ast,
Err(err) => {
return Err(format!(
"failed to parse dependency {}.aleo interface source {}: {err}",
program_name,
source.display()
));
}
};
let (handler, _emitter) = Handler::new_with_buf();
let program = match parse_program(handler, &node_builder, &source_file, &module_source_files, BENCH_NETWORK) {
Ok(ast) => ast,
Err(err) => {
return Err(format!(
"failed to parse dependency {}.aleo interface source {}: {err}",
program_name,
source.display()
));
}
};

// Extract the single program scope.
let scope = match program.program_scopes.values().next() {
Expand Down Expand Up @@ -270,13 +270,17 @@ pub fn load_source_fixture(source_path: &Path) -> Result<FixtureData, String> {
}

/// Creates a fresh [`Compiler`] with all fixture dependencies pre-loaded.
///
/// Uses a buffered emitter so the benchmark measures compile work, not the cost of rendering
/// diagnostics through `ariadne` and writing them to stderr.
pub fn create_compiler(fixture: &FixtureData) -> Compiler {
let expected_unit_name = if fixture.program_name.is_empty() { None } else { Some(fixture.program_name.clone()) };

let (handler, _emitter) = Handler::new_with_buf();
Compiler::new(
expected_unit_name,
false,
Handler::default(),
handler,
Rc::new(NodeBuilder::default()),
PathBuf::default(),
Some(CompilerOptions::default()),
Expand All @@ -289,10 +293,11 @@ pub fn create_compiler(fixture: &FixtureData) -> Compiler {
pub fn create_parse_only_compiler(fixture: &FixtureData) -> Compiler {
let expected_unit_name = if fixture.program_name.is_empty() { None } else { Some(fixture.program_name.clone()) };

let (handler, _emitter) = Handler::new_with_buf();
Compiler::new(
expected_unit_name,
false,
Handler::default(),
handler,
Rc::new(NodeBuilder::default()),
PathBuf::default(),
Some(CompilerOptions::default()),
Expand Down
1 change: 1 addition & 0 deletions crates/compiler/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ impl Compiler {
self.do_pass_with_check::<GlobalItemsCollection, _>((), &mut should_continue)?;
self.do_pass_with_check::<CheckInterfaces, _>((), &mut should_continue)?;
self.do_pass_with_check::<TypeChecking, _>(TypeCheckingInput::new(self.state.network), &mut should_continue)?;
self.do_pass_with_check::<UnusedItems, _>((), &mut should_continue)?;
self.do_pass_with_check::<Disambiguate, _>((), &mut should_continue)?;
self.do_pass_with_check::<CeiAnalyzing, _>((), &mut should_continue)?;
self.do_pass_with_check::<ProcessingAsync, _>(
Expand Down
17 changes: 12 additions & 5 deletions crates/parser/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,23 @@ pub(crate) fn wrong_digit_for_radix_span(
))
}

pub(crate) fn identifier_cannot_start_with_underscore(span: leo_span::Span) -> Formatted {
Formatted::error(CODE_PREFIX, CODE_MASK + 53, "identifiers cannot start with an underscore", span)
.with_help("Rename the identifier so it begins with an ASCII letter.")
}

pub(crate) fn multiple_program_declarations(span: leo_span::Span) -> Formatted {
Formatted::error(CODE_PREFIX, CODE_MASK + 55, "a Leo program can only have one `program` declaration", span)
.with_help("Remove the duplicate `program` block. Only one is allowed per program.")
}

pub(crate) fn binding_name_collides_with_intrinsic(name: impl Display, span: leo_span::Span) -> Formatted {
Formatted::error(
CODE_PREFIX,
CODE_MASK + 56,
format!("binding name `{name}` collides with a compiler intrinsic"),
span,
)
.with_help(
"Calls like `_self_caller()` always dispatch to the intrinsic, ignoring any same-named local. Rename the binding.",
)
}

// Parser warnings

pub(crate) fn record_prototype_redundant(record_name: impl Display, span: leo_span::Span) -> Formatted {
Expand Down
30 changes: 13 additions & 17 deletions crates/parser/src/rowan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,22 +246,24 @@ impl<'a> ConversionContext<'a> {
}
}

/// Validate an identifier in a definition position (struct field, variable,
/// function name, etc.). In addition to the general identifier checks, this
/// rejects identifiers that start with `_` or that are reserved keywords.
fn validate_definition_identifier(&self, ident: &leo_ast::Identifier) {
// Skip validation for error-recovery placeholders.
/// Validate an identifier that introduces a name which never reaches the VM (`let`
/// bindings, tuple-pattern names, `const` declarations). A leading `_` is allowed to
/// signal an intentionally unused binding, matching `rustc`'s `_x` convention.
fn validate_local_binding_identifier(&self, ident: &leo_ast::Identifier) {
if ident.name == Symbol::intern("_error") {
return;
}
self.validate_identifier(ident);
let text = ident.name.to_string();
if text.starts_with('_') {
self.handler.emit_err(crate::errors::identifier_cannot_start_with_underscore(ident.span));
}
if leo_parser_rowan::is_keyword(&text) {
self.emit_unexpected_str("an identifier", &text, ident.span);
}
// Intrinsic call sites (e.g. `_self_caller()`) dispatch by symbol name before any scope
// is consulted, so a same-named binding would be silently shadowed. Reject it here.
// (The empty slice is fine — `from_symbol` still matches; arity is checked later.)
if leo_ast::Intrinsic::from_symbol(ident.name, &[]).is_some() {
self.handler.emit_err(crate::errors::binding_name_collides_with_intrinsic(ident.name, ident.span));
}
}

// =========================================================================
Expand Down Expand Up @@ -1512,12 +1514,6 @@ impl<'a> ConversionContext<'a> {
if name_text.starts_with("sign1") && name_text.parse::<Signature<TestnetV0>>().is_ok() {
return Ok(leo_ast::Literal::signature(name_text, span, id).into());
}
// Reject standalone `_ident` in expression context -- these are only
// valid as the start of intrinsic calls (e.g. `_self_caller()`).
if name_text.starts_with('_') {
self.handler.emit_err(crate::errors::identifier_cannot_start_with_underscore(span));
return Ok(self.error_expression(span));
}
}

Ok(leo_ast::Expression::Path(path))
Expand Down Expand Up @@ -1726,7 +1722,7 @@ impl<'a> ConversionContext<'a> {
match node.kind() {
IDENT_PATTERN => {
let ident = self.require_ident(node, "identifier in pattern");
self.validate_definition_identifier(&ident);
self.validate_local_binding_identifier(&ident);
Ok(leo_ast::DefinitionPlace::Single(ident))
}
TUPLE_PATTERN => {
Expand All @@ -1739,7 +1735,7 @@ impl<'a> ConversionContext<'a> {
leo_ast::Identifier { name: Symbol::intern("_"), span, id: self.builder.next_id() }
} else {
let ident = self.require_ident(&n, "identifier in pattern");
self.validate_definition_identifier(&ident);
self.validate_local_binding_identifier(&ident);
ident
}
})
Expand Down Expand Up @@ -2633,7 +2629,7 @@ impl<'a> ConversionContext<'a> {
let id = self.builder.next_id();

let place = self.require_ident(node, "const name");
self.validate_definition_identifier(&place);
self.validate_local_binding_identifier(&place);

let type_ = self.require_type(node, "const type")?;

Expand Down
1 change: 1 addition & 0 deletions crates/passes/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ pub(crate) mod loop_unroller;
pub(crate) mod name_validation;
pub(crate) mod static_analyzer;
pub(crate) mod type_checker;
pub(crate) mod unused_items;
12 changes: 12 additions & 0 deletions crates/passes/src/errors/name_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ pub(crate) fn illegal_name_content(
)
.with_help(format!("Rename the {item_type} so it does not contain `{keyword}` as a substring."))
}

pub(crate) fn name_starts_with_underscore(item_name: impl Display, item_type: impl Display, span: Span) -> Formatted {
Formatted::error(
CODE_PREFIX,
CODE_MASK + 2,
format!("{item_type} `{item_name}` cannot have a name that starts with `_`"),
span,
)
.with_help(format!(
"{item_type} names are written to the Aleo bytecode and must start with a letter. Rename to avoid the leading underscore."
))
}
11 changes: 11 additions & 0 deletions crates/passes/src/errors/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,17 @@ pub(crate) fn cannot_have_mode(kind: impl Display, span: Span) -> Formatted {
)
}

pub(crate) fn no_inline_not_allowed_on_underscore_fn(span: Span) -> Formatted {
Formatted::error(
CODE_PREFIX,
CODE_MASK + 193,
"`@no_inline` is not allowed on a function whose name starts with `_`",
span,
)
.with_note("A leading `_` forces the function to be inlined, so it cannot also opt out of inlining.")
.with_help("Remove the `@no_inline` annotation or rename the function so it does not start with `_`.")
}

// TypeCheckerWarning builder functions

pub(crate) fn caller_as_record_owner(record_name: impl Display, span: Span) -> Formatted {
Expand Down
52 changes: 52 additions & 0 deletions crates/passes/src/errors/unused_items.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (C) 2019-2026 Provable Inc.
// This file is part of the Leo library.

// The Leo library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// The Leo library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.

//! Warnings for unused items. Wording mirrors `rustc`'s `dead_code`,
//! `unused_variables`, `unused_imports`, and `unreachable_code` lints.

use leo_errors::Formatted;
use leo_span::Span;
use std::fmt::Display;

const CODE_PREFIX: &str = "UNU";
const CODE_MASK: i32 = 14000;

// Warnings

pub(crate) fn unused_function(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK, format!("function `{name}` is never used"), span)
}

pub(crate) fn unused_variable(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK + 1, format!("unused variable: `{name}`"), span)
}

pub(crate) fn unused_struct(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK + 2, format!("struct `{name}` is never constructed"), span)
}

pub(crate) fn unused_import(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK + 3, format!("unused import: `{name}`"), span)
}

pub(crate) fn unused_const(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK + 4, format!("constant `{name}` is never used"), span)
}

pub(crate) fn used_underscore_binding(name: impl Display, span: Span) -> Formatted {
Formatted::warning(CODE_PREFIX, CODE_MASK + 5, format!("used binding `{name}` whose name begins with `_`"), span)
.with_help("Remove the leading `_` from the name, or stop reading the binding.")
}
6 changes: 6 additions & 0 deletions crates/passes/src/function_inlining/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ impl AstReconstructor for TransformVisitor<'_> {
self.always_inline.contains(&vec![callee.identifier.name]),
"this function has been called from another function",
) ||
// A leading `_` marks the function intentionally unused; force-inline so the
// name never reaches the VM as a closure identifier.
mandatory_cond(
crate::unused_items::name_starts_with_underscore(callee.identifier.name),
"this function name starts with `_`",
) ||
// Called only once
optional_cond(*call_count_ref == 1) ||
// Has no arguments
Expand Down
3 changes: 3 additions & 0 deletions crates/passes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub use type_checking::*;
mod name_validation;
pub use name_validation::*;

mod unused_items;
pub use unused_items::*;

mod check_interfaces;
pub use check_interfaces::*;

Expand Down
32 changes: 27 additions & 5 deletions crates/passes/src/name_validation/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ impl UnitVisitor for NameValidationVisitor<'_> {
let program_name = input.program_id.name;
self.does_not_contain_aleo(program_name, "program");
self.is_not_keyword(program_name, "program", &[]);
self.does_not_start_with_underscore(program_name, "program");

input.composites.iter().for_each(|(_, function)| self.visit_composite(function));
input.interfaces.iter().for_each(|(_, interface)| self.visit_interface(interface));
input.mappings.iter().for_each(|(_, m)| self.visit_mapping(m));
input.storage_variables.iter().for_each(|(_, s)| self.visit_storage_variable(s));
input.functions.iter().for_each(|(_, function)| self.visit_function(function));
}

Expand All @@ -53,27 +56,46 @@ impl UnitVisitor for NameValidationVisitor<'_> {
if input.is_record {
self.does_not_contain_aleo(composite_name, item_type);
}
self.does_not_start_with_underscore(composite_name, item_type);

for Member { identifier: member_name, .. } in &input.members {
let member_item_type = if input.is_record { "record member" } else { "struct member" };
if input.is_record {
self.is_not_keyword(*member_name, "record member", &["owner"]);
self.does_not_contain_aleo(*member_name, "record member");
self.is_not_keyword(*member_name, member_item_type, &["owner"]);
self.does_not_contain_aleo(*member_name, member_item_type);
} else {
self.is_not_keyword(*member_name, "struct member", &[]);
self.is_not_keyword(*member_name, member_item_type, &[]);
}
self.does_not_start_with_underscore(*member_name, member_item_type);
}
}

fn visit_function(&mut self, function: &Function) {
use Variant::*;
match function.variant {
EntryPoint => self.is_not_keyword(function.identifier, "entry point fn", &[]),
EntryPoint => {
self.is_not_keyword(function.identifier, "entry point fn", &[]);
// Only entry points and `view fn`s emit their name verbatim to the VM; the other
// variants are always inlined, so the underscore check applies only to these two.
self.does_not_start_with_underscore(function.identifier, "entry-point fn");
}
View => {
self.is_not_keyword(function.identifier, "view fn", &[]);
self.does_not_start_with_underscore(function.identifier, "view fn");
}
Fn => self.is_not_keyword(function.identifier, "regular fn", &[]),
View => self.is_not_keyword(function.identifier, "view fn", &[]),
FinalFn | Finalize => {}
}
}

fn visit_mapping(&mut self, input: &Mapping) {
self.does_not_start_with_underscore(input.identifier, "mapping");
}

fn visit_storage_variable(&mut self, input: &StorageVariable) {
self.does_not_start_with_underscore(input.identifier, "storage variable");
}

fn visit_function_stub(&mut self, input: &FunctionStub) {
use Variant::*;
match input.variant {
Expand Down
9 changes: 9 additions & 0 deletions crates/passes/src/name_validation/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ impl NameValidationVisitor<'_> {
}
}

/// Reject names starting with `_` at positions where the name reaches the Aleo VM, which
/// requires identifiers to start with a letter.
pub fn does_not_start_with_underscore(&self, name: Identifier, item_type: &str) {
if crate::unused_items::name_starts_with_underscore(name.name) {
self.handler
.emit_err(crate::errors::name_validation::name_starts_with_underscore(name, item_type, name.span));
}
}

pub fn is_not_keyword(&self, name: Identifier, item_type: &str, whitelist: &[&str]) {
// Flatten RESTRICTED_KEYWORDS by ignoring ConsensusVersion
let restricted = Program::<TestnetV0>::RESTRICTED_KEYWORDS.iter().flat_map(|(_, kws)| kws.iter().copied());
Expand Down
25 changes: 20 additions & 5 deletions crates/passes/src/path_resolution/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,26 @@ impl AstReconstructor for PathResolutionVisitor<'_> {
members: input
.members
.into_iter()
.map(|member| CompositeFieldInitializer {
identifier: member.identifier,
expression: member.expression.map(|expr| self.reconstruct_expression(expr, &()).0),
span: member.span,
id: member.id,
.map(|member| {
// Desugar composite-init shorthand `Foo { a }` into `Foo { a: <resolved
// path>}` whenever `a` resolves to a local binding or a top-level
// (program-scope) global. Mirrors the type checker's shorthand lookup
// in `type_checking::ast::visit_composite_init` — locals first, then
// current-program globals at the program (not module) scope. Later
// passes and the unused-items analysis then see a fully-resolved
// expression instead of a bare identifier with no target. When neither
// resolution succeeds the shorthand stays as `None` and the type
// checker emits its focused diagnostic.
let expression = match member.expression {
Some(expr) => Some(self.reconstruct_expression(expr, &()).0),
None => self.resolve_shorthand(member.identifier).map(Expression::Path),
};
CompositeFieldInitializer {
identifier: member.identifier,
expression,
span: member.span,
id: member.id,
}
})
.collect(),
..input
Expand Down
Loading
Loading