Skip to content

Grammar.ebnf generator#1027

Draft
Mte90 wants to merge 7 commits intostagingfrom
grammar
Draft

Grammar.ebnf generator#1027
Mte90 wants to merge 7 commits intostagingfrom
grammar

Conversation

@Mte90
Copy link
Member

@Mte90 Mte90 commented Feb 13, 2026

So I did a lot of changes because my plan was to create something that fill the file automatically using macros.

So part of the file is hardcoded and another part is updated automatically.

It requires a careful review because I changed a lot of files.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added grammar generation command to export EBNF language specifications.
  • Chores

    • Enhanced internal keyword system for improved language consistency.

@Mte90 Mte90 requested review from Ph0enixKM and Copilot February 13, 2026 11:22
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch grammar

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an auto-discovery system for Amber language keywords (via a new AutoKeyword derive + inventory registry) and adds a CLI subcommand to generate a grammar.ebnf file from a partially hardcoded EBNF template plus build-time-generated keyword/builtin sections.

Changes:

  • Add amber_meta::AutoKeyword derive macro and a runtime keyword registry (modules::keywords) backed by inventory.
  • Introduce utils::grammar_ebnf::generate_grammar_ebnf() and a GrammarEbnf CLI subcommand to emit grammar.ebnf.
  • Annotate many syntax modules (statements/builtins/operators) with #[derive(AutoKeyword)] and add inventory dependencies.

Reviewed changes

Copilot reviewed 60 out of 62 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/utils/mod.rs Exposes the new grammar_ebnf utility module.
src/utils/grammar_ebnf.rs Adds EBNF template + generator that injects discovered keywords/builtins.
src/tests/testing.rs Removes trailing whitespace line.
src/modules/variable/mod.rs Registers several statement keywords via AutoKeyword.
src/modules/variable/init.rs Registers let/const keywords via AutoKeyword.
src/modules/test.rs Registers test keyword via AutoKeyword.
src/modules/prelude.rs Re-exports AutoKeyword for convenient use across modules.
src/modules/mod.rs Adds the new modules::keywords module.
src/modules/main.rs Registers main keyword via AutoKeyword.
src/modules/loops/while_loop.rs Registers while keyword via AutoKeyword.
src/modules/loops/iter_loop.rs Registers for keyword via AutoKeyword.
src/modules/loops/infinite_loop.rs Registers loop keyword via AutoKeyword.
src/modules/loops/continue_stmt.rs Registers continue keyword via AutoKeyword.
src/modules/loops/break_stmt.rs Registers break keyword via AutoKeyword.
src/modules/keywords.rs Introduces KeywordKind, KeywordRegistration, and iter_keywords() using inventory.
src/modules/imports/import.rs Registers import/pub keywords via AutoKeyword.
src/modules/function/ret.rs Registers return keyword via AutoKeyword.
src/modules/function/fail.rs Registers fail keyword via AutoKeyword.
src/modules/function/declaration.rs Registers ref/fun keywords via AutoKeyword.
src/modules/expression/unop/not.rs Registers not keyword via AutoKeyword.
src/modules/expression/literal/status.rs Registers status keyword via AutoKeyword.
src/modules/expression/binop/add.rs Registers a binary-op keyword entry for the add operator module.
src/modules/expression/binop/and.rs Registers and as a binary-op keyword.
src/modules/expression/binop/div.rs Registers a binary-op keyword entry for the div operator module.
src/modules/expression/binop/eq.rs Registers a binary-op keyword entry for the eq operator module.
src/modules/expression/binop/ge.rs Registers a binary-op keyword entry for the ge operator module.
src/modules/expression/binop/gt.rs Registers a binary-op keyword entry for the gt operator module.
src/modules/expression/binop/le.rs Registers a binary-op keyword entry for the le operator module.
src/modules/expression/binop/lt.rs Registers a binary-op keyword entry for the lt operator module.
src/modules/expression/binop/modulo.rs Registers a binary-op keyword entry for the modulo operator module.
src/modules/expression/binop/mul.rs Registers a binary-op keyword entry for the mul operator module.
src/modules/expression/binop/neq.rs Registers a binary-op keyword entry for the neq operator module.
src/modules/expression/binop/or.rs Registers or as a binary-op keyword.
src/modules/expression/binop/range.rs Registers a binary-op keyword entry for the range operator module.
src/modules/expression/binop/sub.rs Registers a binary-op keyword entry for the sub operator module.
src/modules/condition/ifcond.rs Registers if via AutoKeyword.
src/modules/condition/ifchain.rs Registers if via AutoKeyword and reshuffles DocumentationModule impl placement.
src/modules/condition/failure_handler.rs Registers failed/succeeded/exited keywords via AutoKeyword.
src/modules/command/modifier.rs Registers command modifier keywords (sudo, silent, etc.) via AutoKeyword.
src/modules/builtin/wait.rs Registers await builtin via AutoKeyword.
src/modules/builtin/touch.rs Registers touch builtin via AutoKeyword.
src/modules/builtin/sleep.rs Registers sleep builtin via AutoKeyword.
src/modules/builtin/rm.rs Registers rm builtin via AutoKeyword.
src/modules/builtin/pwd.rs Registers pwd builtin via AutoKeyword.
src/modules/builtin/pid.rs Registers pid builtin via AutoKeyword.
src/modules/builtin/nameof.rs Registers nameof builtin via AutoKeyword.
src/modules/builtin/mv.rs Registers mv builtin via AutoKeyword.
src/modules/builtin/ls.rs Registers ls builtin via AutoKeyword.
src/modules/builtin/lines.rs Registers lines builtin via AutoKeyword.
src/modules/builtin/len.rs Registers len builtin via AutoKeyword.
src/modules/builtin/exit.rs Registers exit builtin via AutoKeyword.
src/modules/builtin/echo.rs Registers echo builtin via AutoKeyword.
src/modules/builtin/disown.rs Registers disown builtin via AutoKeyword.
src/modules/builtin/cp.rs Registers cp builtin via AutoKeyword.
src/modules/builtin/clear.rs Registers clear builtin via AutoKeyword.
src/modules/builtin/cd.rs Registers cd builtin via AutoKeyword.
src/main.rs Adds a GrammarEbnf CLI subcommand that writes grammar.ebnf.
meta/src/lib.rs Extends proc-macro crate: refactors Context macros and adds AutoKeyword derive (inventory submit).
meta/src/dispatch.rs Marks dispatch generator hidden in docs.
meta/Cargo.toml Adds inventory dependency to proc-macro crate.
Cargo.toml Adds inventory dependency to main crate.
Cargo.lock Locks inventory and its transitive deps.
.gitignore Ignores .sisyphus.
Comments suppressed due to low confidence (1)

src/modules/expression/binop/add.rs:38

  • These BinOp implementations register keywords like "add", but the actual parsed operator token is "+" (parse_operator() calls token(meta, "+")). This will put non-existent user-facing keywords into the keyword registry (and potentially into generated artifacts if filtering changes). Either avoid AutoKeyword on symbolic operators, or register the real operator lexeme in a way the downstream generator can handle (likely separate from KEYWORD_*).
#[derive(Debug, Clone, AutoKeyword)]
#[keyword = "add"]
#[kind = "binary_op"]
pub struct Add {
    left: Box<Expr>,
    right: Box<Expr>,
    kind: Type,
}

impl Typed for Add {
    fn get_type(&self) -> Type {
        self.kind.clone()
    }
}

impl BinOp for Add {
    fn set_left(&mut self, left: Expr) {
        *self.left = left;
    }

    fn set_right(&mut self, right: Expr) {
        *self.right = right;
    }

    fn parse_operator(&mut self, meta: &mut ParserMetadata) -> SyntaxResult {
        token(meta, "+")?;
        Ok(())
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +191 to +251
// Collect unique keywords using HashSet to deduplicate
let mut keyword_defs = String::new();
let mut keyword_list: Vec<_> = keywords.into_iter().collect();
// Sort for deterministic output
keyword_list.sort();
for kw in keyword_list {
keyword_defs.push_str(&format!("KEYWORD_{} = '{}' ;\n", kw.to_uppercase(), kw));
}
keyword_defs.push('\n');

// Generate individual builtin rules for statement builtins
let mut builtin_stmt_rules = String::new();
for kw in &builtin_stmt_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"clear" | "pid" => {
builtin_stmt_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_stmt_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
}
builtin_stmt_rules.push('\n');

// Generate individual builtin rules for expression builtins
let mut builtin_expr_rules = String::new();
for kw in &builtin_expr_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"pid" => {
builtin_expr_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_expr_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
}
builtin_expr_rules.push('\n');

// Generate builtins_statement rule
let builtins_stmt_rule = if builtin_stmt_keywords.is_empty() {
String::new()
} else {
let alternatives: Vec<String> = builtin_stmt_keywords
.iter()
.map(|kw| format!("builtin_{}", kw))
.collect();
format!("builtins_statement = {} ;\n\n", alternatives.join(" | "))
};

// Generate builtins_expression rule
let builtins_expr_rule = if builtin_expr_keywords.is_empty() {
String::new()
} else {
let alternatives: Vec<String> = builtin_expr_keywords
.iter()
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated builtin rules assume a uniform KEYWORD_X, expression shape (and special-case clear/pid), but several builtins have different concrete syntax in the parser: e.g. cp(...) takes 2–3 comma-separated args and requires a failure handler; rm(...) takes 1–3 args; pwd()/pid() require empty parentheses; ls(...) has 0–3 args + failure handler. As written, generate_grammar_ebnf() will generate rules that don't match the language. Consider encoding per-builtin signatures (args/parentheses/handler) in the registry (extra derive attributes) or generate these rules directly from the parsing definitions instead of hardcoding KEYWORD_X, expression.

Copilot uses AI. Check for mistakes.
src/main.rs Outdated
CommandKind::GrammarEbnf => {
let output = grammar_ebnf::generate_grammar_ebnf();
let output_path = PathBuf::from("grammar.ebnf");
std::fs::write(&output_path, output).expect("Failed to write grammar.ebnf");
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GrammarEbnf uses std::fs::write(...).expect(...), which will panic instead of returning an error through main()’s Result. Prefer propagating the I/O error with ? (or mapping it into the existing error type) so the CLI exits cleanly with a message and non-zero status.

Suggested change
std::fs::write(&output_path, output).expect("Failed to write grammar.ebnf");
std::fs::write(&output_path, output)?;

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +274
/// Parse the `#[kind = "..."]` attribute
fn parse_kind_attribute(attrs: &[Attribute]) -> Option<&'static str> {
for attr in attrs {
if attr.path().is_ident("kind") {
let meta = &attr.meta;
if let syn::Meta::NameValue(name_value) = meta {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &name_value.value
{
return Some(lit_str.value().leak());
}
}
}
}
None
}

/// Parse the `#[keyword = "..."]` attribute
fn parse_keyword_attribute(attrs: &[Attribute]) -> Option<&'static str> {
for attr in attrs {
if attr.path().is_ident("keyword") {
// Parse the attribute value
let meta = &attr.meta;
if let syn::Meta::NameValue(name_value) = meta {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &name_value.value
{
return Some(lit_str.value().leak());
}
}
}
}
None
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_kind_attribute() / parse_keyword_attribute() convert syn::LitStr to &'static str via String::leak(). Even though this runs in the proc-macro process, leaking per derive invocation is avoidable and can bloat the compiler process on large crates. Prefer returning an owned String (or LitStr) and carrying it through codegen without leaking.

Copilot uses AI. Check for mistakes.
Mte90 and others added 4 commits February 13, 2026 12:33
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Mte90
Copy link
Member Author

Mte90 commented Feb 13, 2026

@Ph0enixKM I think that requires a real review especially for the grammar_ebnf.rs and keyword.rs because the other changes are to add the macros in the various files.

I see also that Copilot report that the grammar.ebnf as it is today, is not following really the standard.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modules/function/fail.rs (1)

12-18: ⚠️ Potential issue | 🟡 Minor

Make is_main private.
This field is only accessed internally within the Fail implementation and should not be part of the public API. Remove the pub visibility modifier entirely.

♻️ Suggested visibility fix
 pub struct Fail {
     pub expr: Expr,
     pub code: String,
-    pub is_main: bool,
+    is_main: bool,
 }
🤖 Fix all issues with AI agents
In `@meta/src/lib.rs`:
- Line 10: After parsing the input as a DeriveInput in the context_manager and
context_helper macros, add a compile-time guard that checks that input.data is
syn::Data::Struct and returns a syn::Error compiled into a TokenStream if not;
update both macros in meta/src/lib.rs (the functions that call
parse_macro_input::<DeriveInput>) to emit Error::new_spanned(...) ->
to_compile_error().into() when the data is not a struct so
ManagerVisitor/HelperVisitor won’t silently produce empty impls for enums or
unions.

In `@src/main.rs`:
- Around line 163-211: Remove the duplicate main() (the first definition) and
keep the second main that calls .with_env_vars(); then add a
CommandKind::GrammarEbnf arm into that retained main which mirrors the removed
one by calling grammar_ebnf::generate_grammar_ebnf(), writing the result to
PathBuf::from("grammar.ebnf") with std::fs::write (with the same error
handling/expect message), and returning 0 as the exit code; ensure the other
command arms (e.g., CommandKind::Build, CommandKind::Docs,
CompilerOptions::from_args, execute_output) remain unchanged and the program
still calls std::process::exit(exit_code) at the end.

In `@src/modules/expression/literal/status.rs`:
- Around line 10-12: The Status literal is incorrectly annotated as a statement:
remove the #[kind = "stmt"] attribute from the Status declaration so it matches
other literals (Bool, Integer, Null, Number, Text, Array) and remains an
expression that implements Typed (returns Type::Int) and translates to
VarExprFragment; if a kind is required by the macro, change it to the
expression-appropriate kind instead of "stmt".

In `@src/modules/keywords.rs`:
- Around line 5-9: Clippy warns about the blank line after the outer attribute
#[allow(dead_code)]; remove the empty line so the doc comment (the /// The kind
of keyword ...) immediately follows the attribute, or if the documentation is
meant for the module convert the outer attribute into an inner one
(#![allow(dead_code)]) and keep the module-level doc comments as-is; update the
code around #[allow(dead_code)] and the subsequent doc comment accordingly.

In `@src/utils/grammar_ebnf.rs`:
- Around line 201-231: The generator in grammar_ebnf.rs is emitting a
one-argument rule for all builtins (loop over builtin_stmt_keywords /
builtin_expr_keywords) which misrepresents multi-arg builtins like Mv::parse and
produces builtin_mv = KEYWORD_MV, expression ; — fix by introducing per-builtin
signature metadata (e.g., a map from keyword to
signature/arity/parentheses/optional modifiers) and update the loop that builds
builtin_name rules to consult that metadata; for "mv" emit a rule matching its
real signature (include optional modifiers and two expressions/parenthesized
expressions, e.g. builtin_mv = KEYWORD_MV, modifiers?, expression, expression ;
or the project’s grammar-equivalent), and extend the metadata for other
multi-arg builtins so generated rules reflect correct arity and optional
parentheses.
🧹 Nitpick comments (5)
meta/src/lib.rs (1)

237-254: Avoid leaking strings during macro expansion.
lit_str.value().leak() at lines 248 and 268 permanently allocates in the compiler process. Returning String is sufficient and works correctly with the quote! macro, which properly interpolates string values to &'static str literals in generated code.

♻️ Refactor to return `String` instead of leaking
-fn parse_kind_attribute(attrs: &[Attribute]) -> Option<&'static str> {
+fn parse_kind_attribute(attrs: &[Attribute]) -> Option<String> {
     for attr in attrs {
         if attr.path().is_ident("kind") {
             let meta = &attr.meta;
             if let syn::Meta::NameValue(name_value) = meta {
                 if let syn::Expr::Lit(syn::ExprLit {
                     lit: syn::Lit::Str(lit_str),
                     ..
                 }) = &name_value.value
                 {
-                    return Some(lit_str.value().leak());
+                    return Some(lit_str.value());
                 }
             }
         }
     }
     None
 }
 
-fn parse_keyword_attribute(attrs: &[Attribute]) -> Option<&'static str> {
+fn parse_keyword_attribute(attrs: &[Attribute]) -> Option<String> {
     for attr in attrs {
         if attr.path().is_ident("keyword") {
             let meta = &attr.meta;
             if let syn::Meta::NameValue(name_value) = meta {
                 if let syn::Expr::Lit(syn::ExprLit {
                     lit: syn::Lit::Str(lit_str),
                     ..
                 }) = &name_value.value
                 {
-                    return Some(lit_str.value().leak());
+                    return Some(lit_str.value());
                 }
             }
         }
     }
     None
 }

Also applies to: lines 256-274

src/modules/builtin/sleep.rs (1)

9-11: Minor formatting inconsistency: trailing space in attribute.

Line 11 has a trailing space before the closing bracket (#[kind = "builtin_stmt" ]), while other files in this PR don't have this inconsistency. This is a nitpick but worth keeping formatting consistent across the codebase.

Note: Unlike other files in this PR (e.g., status.rs, and.rs), this file doesn't explicitly import amber_meta::AutoKeyword and relies on the prelude re-export. Both approaches work, but consider being consistent across the codebase.

src/modules/builtin/ls.rs (1)

11-13: Minor formatting: trailing space in attribute.

Same minor inconsistency as in sleep.rs - trailing space before closing bracket on line 13 (#[kind = "builtin_expr" ]). The keyword and kind values are correct and match the implementation.

src/modules/condition/ifchain.rs (1)

15-18: Consider keeping cond_blocks / false_block encapsulated.

Making these fields pub widens the mutation surface; if external access is required, accessors or a focused API can preserve invariants.

src/utils/grammar_ebnf.rs (1)

180-189: Sort/dedup builtin keyword lists for stable grammar output.

keyword_defs is deterministic, but builtin lists preserve iteration order. Sorting + deduping avoids duplicate rule definitions and makes the generated grammar stable across builds.

Suggested change
-    let builtin_stmt_keywords: Vec<&str> = iter_keywords()
+    let mut builtin_stmt_keywords: Vec<&str> = iter_keywords()
         .filter(|r| r.kind == KeywordKind::BuiltinStmt)
         .map(|r| r.keyword)
         .collect();
+    builtin_stmt_keywords.sort();
+    builtin_stmt_keywords.dedup();

-    let builtin_expr_keywords: Vec<&str> = iter_keywords()
+    let mut builtin_expr_keywords: Vec<&str> = iter_keywords()
         .filter(|r| r.kind == KeywordKind::BuiltinExpr)
         .map(|r| r.keyword)
         .collect();
+    builtin_expr_keywords.sort();
+    builtin_expr_keywords.dedup();

use proc_macro::TokenStream;
use syn::visit::Visit;
use syn::{parse_macro_input, DeriveInput, ItemStruct};
use syn::{parse_macro_input, Attribute, DeriveInput};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the meta/src/lib.rs file to see the current implementation
cat -n meta/src/lib.rs

Repository: amber-lang/amber

Length of output: 10775


🏁 Script executed:

# Check the ManagerVisitor implementation
cat -n meta/src/manager.rs | head -100

Repository: amber-lang/amber

Length of output: 4392


🏁 Script executed:

# Continue reading manager.rs to see visit_data implementation
cat -n meta/src/manager.rs | tail -20

Repository: amber-lang/amber

Length of output: 849


🏁 Script executed:

# Check if there's a visit_data method in manager.rs
grep -n "visit_data" meta/src/manager.rs

Repository: amber-lang/amber

Length of output: 42


🏁 Script executed:

# Check helper.rs to see HelperVisitor implementation
cat -n meta/src/helper.rs

Repository: amber-lang/amber

Length of output: 1746


🏁 Script executed:

# Search for actual usages of #[derive(ContextManager)] and #[derive(ContextHelper)]
rg -n "#\[derive.*ContextManager\]" -A 1
rg -n "#\[derive.*ContextHelper\]" -A 1

Repository: amber-lang/amber

Length of output: 42


🏁 Script executed:

# Try broader search for ContextManager and ContextHelper derives
rg "ContextManager|ContextHelper" --type rust -B 3 -A 1

Repository: amber-lang/amber

Length of output: 4931


🏁 Script executed:

# Check if visit_data is defined in syn or is custom
grep -rn "visit_data" meta/src/

Repository: amber-lang/amber

Length of output: 160


🏁 Script executed:

# Check the syn crate to understand visit_data - let's look at what it does
# First, let's see if there's a Cargo.toml in meta to check syn version
cat meta/Cargo.toml | grep -A 5 "\[dependencies\]"

Repository: amber-lang/amber

Length of output: 186


🏁 Script executed:

# Let's write a quick test to understand how syn's Visit trait behaves with enums
python3 << 'EOF'
# Check syn documentation for Visit trait behavior
# The Visit trait has default implementations that dispatch based on the data kind
# For enums, visit_data calls visit_enum_data which iterates over variants
# Since the visitors only implement visit_field, enums would be silently processed with empty output

# Let's verify that no struct guard exists by checking the exact flow
print("Verification of concern:")
print("1. context_manager parses as DeriveInput (lines 87-94)")
print("2. No explicit guard checking data is Struct")
print("3. Calls visitor.visit_data(&data)")
print("4. ManagerVisitor only implements visit_field")
print("5. For enums, visit_data would dispatch to visit_enum_data")
print("6. This would visit variants instead of fields")
print("7. Since no visit_variant is implemented, nothing happens")
print("8. Macro silently generates empty impl with no error")
print("\nAll current usages are on structs, so problem is latent")
EOF

Repository: amber-lang/amber

Length of output: 539


🏁 Script executed:

# Let's verify the exact behavior by checking if there's any safety check we missed
grep -n "matches!" meta/src/lib.rs

Repository: amber-lang/amber

Length of output: 42


Add explicit struct-only guards to context_manager and context_helper.

Both macros currently parse as DeriveInput without checking that the input is actually a struct. If applied to an enum or union, the visitors would iterate over variants or union fields respectively, and since ManagerVisitor and HelperVisitor only implement visit_field, the macros would silently generate empty implementations with no error. Add a compile-time check to ensure these macros only work on structs.

Suggested guard to preserve the prior behavior
 pub fn context_manager(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
     let syn::DeriveInput { data, ident, .. } = input;
+    if !matches!(data, syn::Data::Struct(_)) {
+        return syn::Error::new_spanned(
+            &ident,
+            "ContextManager can only be derived for structs",
+        )
+        .to_compile_error()
+        .into();
+    }
     let mut visitor = ManagerVisitor::new(&ident);
     visitor.visit_data(&data);

Also applies to: lines 100-106 (context_helper)

🤖 Prompt for AI Agents
In `@meta/src/lib.rs` at line 10, After parsing the input as a DeriveInput in the
context_manager and context_helper macros, add a compile-time guard that checks
that input.data is syn::Data::Struct and returns a syn::Error compiled into a
TokenStream if not; update both macros in meta/src/lib.rs (the functions that
call parse_macro_input::<DeriveInput>) to emit Error::new_spanned(...) ->
to_compile_error().into() when the data is not a struct so
ManagerVisitor/HelperVisitor won’t silently produce empty impls for enums or
unions.

Comment on lines +10 to +12
#[derive(Debug, Clone, AutoKeyword)]
#[keyword = "status"]
#[kind = "stmt"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/modules/expression/literal/status.rs

Repository: amber-lang/amber

Length of output: 2027


🏁 Script executed:

# Find all kind declarations in the codebase to understand patterns
rg -n '#\[kind\s*=' --type rust -B2 | head -100

Repository: amber-lang/amber

Length of output: 4613


🏁 Script executed:

# Search for Pwd and other builtin expressions
rg -n '#\[kind' --type rust src/modules/expression/literal/ src/modules/expression/binop/ | head -50

Repository: amber-lang/amber

Length of output: 973


🏁 Script executed:

# Let's look for Pwd specifically
find . -name "pwd.rs" -type f

Repository: amber-lang/amber

Length of output: 88


🏁 Script executed:

# More broadly search for builtin_expr or expr kind values
rg -n '#\[kind\s*=\s*".*expr' --type rust

Repository: amber-lang/amber

Length of output: 399


🏁 Script executed:

# Check if there are any other literals in expression/literal/ to see their kind
ls -la src/modules/expression/literal/

Repository: amber-lang/amber

Length of output: 634


🏁 Script executed:

# Look at the structure of other files in expression/literal
for file in src/modules/expression/literal/*.rs; do echo "=== $file ==="; grep -E '#\[kind|impl.*Typed' "$file"; done

Repository: amber-lang/amber

Length of output: 621


🏁 Script executed:

# Check the actual content of a few other literal files to see if any have kind attributes
cat -n src/modules/expression/literal/bool.rs

Repository: amber-lang/amber

Length of output: 1598


🏁 Script executed:

cat -n src/modules/expression/literal/integer.rs | head -20

Repository: amber-lang/amber

Length of output: 638


Remove or correct the kind attribute for Status.

Status is an expression literal that implements Typed (returns Type::Int) and translates to a VarExprFragment. Marking it as kind = "stmt" is semantically incorrect—statements define control flow or declarations, not expressions that return values.

All other literals in src/modules/expression/literal/ (Bool, Integer, Null, Number, Text, Array) omit the kind attribute entirely and follow the same pattern. Status should either match this convention by removing the attribute, or if a kind is required, be classified as an expression type rather than a statement.

🤖 Prompt for AI Agents
In `@src/modules/expression/literal/status.rs` around lines 10 - 12, The Status
literal is incorrectly annotated as a statement: remove the #[kind = "stmt"]
attribute from the Status declaration so it matches other literals (Bool,
Integer, Null, Number, Text, Array) and remains an expression that implements
Typed (returns Type::Int) and translates to VarExprFragment; if a kind is
required by the macro, change it to the expression-appropriate kind instead of
"stmt".

Comment on lines +5 to +9
#[allow(dead_code)]

/// The kind of keyword (statement, builtin, or binary operation)
/// Also supports dynamic string-based kinds like "binary_op", "builtin_stmt", "builtin_expr", etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the empty line after the outer attribute (Clippy).

Clippy flags “empty line after outer attribute.” Place the doc comment immediately after the attribute (or convert to an inner attribute if it’s meant for the module).

Suggested fix
-#[allow(dead_code)]
-
-/// The kind of keyword (statement, builtin, or binary operation)
+#[allow(dead_code)]
+/// The kind of keyword (statement, builtin, or binary operation)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[allow(dead_code)]
/// The kind of keyword (statement, builtin, or binary operation)
/// Also supports dynamic string-based kinds like "binary_op", "builtin_stmt", "builtin_expr", etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
/// The kind of keyword (statement, builtin, or binary operation)
/// Also supports dynamic string-based kinds like "binary_op", "builtin_stmt", "builtin_expr", etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
🧰 Tools
🪛 GitHub Check: Clippy tests

[failure] 5-5:
empty line after outer attribute

🤖 Prompt for AI Agents
In `@src/modules/keywords.rs` around lines 5 - 9, Clippy warns about the blank
line after the outer attribute #[allow(dead_code)]; remove the empty line so the
doc comment (the /// The kind of keyword ...) immediately follows the attribute,
or if the documentation is meant for the module convert the outer attribute into
an inner one (#![allow(dead_code)]) and keep the module-level doc comments
as-is; update the code around #[allow(dead_code)] and the subsequent doc comment
accordingly.

Comment on lines +201 to +231
// Generate individual builtin rules for statement builtins
let mut builtin_stmt_rules = String::new();
for kw in &builtin_stmt_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"clear" | "pid" => {
builtin_stmt_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_stmt_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
}
builtin_stmt_rules.push('\n');

// Generate individual builtin rules for expression builtins
let mut builtin_expr_rules = String::new();
for kw in &builtin_expr_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"pid" => {
builtin_expr_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_expr_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Generated builtin rules don’t match mv’s real signature (modifiers + 2 args).

Mv::parse accepts command modifiers and two expressions (optionally parenthesized), but the generator emits builtin_mv = KEYWORD_MV, expression ;. The produced grammar will reject valid syntax and misdocument mv (and likely other multi‑arg builtins). Consider encoding per‑builtin signatures (or metadata) so rules reflect real arity/parentheses and optional modifiers.

Example fix pattern (extend for other builtins as needed)
-            _ => builtin_stmt_rules.push_str(&format!(
-                "{} = KEYWORD_{}, expression ;\n",
-                builtin_name, kw_upper
-            )),
+            "mv" => builtin_stmt_rules.push_str(&format!(
+                "{} = command_modifier, KEYWORD_{}, ( '(', expression, ',', expression, ')' | expression, expression ) ;\n",
+                builtin_name, kw_upper
+            )),
+            _ => builtin_stmt_rules.push_str(&format!(
+                "{} = command_modifier, KEYWORD_{}, expression ;\n",
+                builtin_name, kw_upper
+            )),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Generate individual builtin rules for statement builtins
let mut builtin_stmt_rules = String::new();
for kw in &builtin_stmt_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"clear" | "pid" => {
builtin_stmt_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_stmt_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
}
builtin_stmt_rules.push('\n');
// Generate individual builtin rules for expression builtins
let mut builtin_expr_rules = String::new();
for kw in &builtin_expr_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"pid" => {
builtin_expr_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_expr_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
// Generate individual builtin rules for statement builtins
let mut builtin_stmt_rules = String::new();
for kw in &builtin_stmt_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"clear" | "pid" => {
builtin_stmt_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
"mv" => builtin_stmt_rules.push_str(&format!(
"{} = command_modifier, KEYWORD_{}, ( '(', expression, ',', expression, ')' | expression, expression ) ;\n",
builtin_name, kw_upper
)),
_ => builtin_stmt_rules.push_str(&format!(
"{} = command_modifier, KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
}
builtin_stmt_rules.push('\n');
// Generate individual builtin rules for expression builtins
let mut builtin_expr_rules = String::new();
for kw in &builtin_expr_keywords {
let kw_upper = kw.to_uppercase();
let builtin_name = format!("builtin_{}", kw);
match kw.as_ref() {
"pid" => {
builtin_expr_rules.push_str(&format!("{} = KEYWORD_{} ;\n", builtin_name, kw_upper))
}
_ => builtin_expr_rules.push_str(&format!(
"{} = KEYWORD_{}, expression ;\n",
builtin_name, kw_upper
)),
}
🤖 Prompt for AI Agents
In `@src/utils/grammar_ebnf.rs` around lines 201 - 231, The generator in
grammar_ebnf.rs is emitting a one-argument rule for all builtins (loop over
builtin_stmt_keywords / builtin_expr_keywords) which misrepresents multi-arg
builtins like Mv::parse and produces builtin_mv = KEYWORD_MV, expression ; — fix
by introducing per-builtin signature metadata (e.g., a map from keyword to
signature/arity/parentheses/optional modifiers) and update the loop that builds
builtin_name rules to consult that metadata; for "mv" emit a rule matching its
real signature (include optional modifiers and two expressions/parenthesized
expressions, e.g. builtin_mv = KEYWORD_MV, modifiers?, expression, expression ;
or the project’s grammar-equivalent), and extend the metadata for other
multi-arg builtins so generated rules reflect correct arity and optional
parentheses.

@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 33.74233% with 108 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/utils/grammar_ebnf.rs 0.00% 73 Missing ⚠️
src/modules/keywords.rs 0.00% 21 Missing ⚠️
meta/src/lib.rs 88.70% 7 Missing ⚠️
src/main.rs 0.00% 4 Missing ⚠️
src/modules/condition/ifchain.rs 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

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