amber-lang / amber-lang/bash2amber
[Detail Bug] Amber render: non-local variable assignments missed for multi-assign and export in Bash simple commands
- Langage dominant
- Rust
- Étoiles
- 2
- Forks
- 0
- Métriques de merge des PR
- Aucune PR mergée en 30 j
Description
# Summary
- **Context**: `src/amber/render/analysis.rs` provides static analysis of Bash commands to identify non-local variable assignments and variable references, which are used to determine which variables must be declared as globals in Amber.
- **Bug**: The `collect_non_local_assignments_from_command` function only inspects the first word of a `SimpleCommand`, missing subsequent assignments in the same line and assignments following builtins like `export`.
- **Actual vs. expected**: It only checks `simple.words.first()`, whereas it should iterate through all leading words that are valid assignments in a Bash simple command.
- **Impact**: Variables that are global in Bash are incorrectly identified as local to a function (or omitted from global declaration), leading to broken variable visibility and incorrect Amber code generation.
# Code with bug
```rust
fn collect_non_local_assignments_from_command(
command: &Command,
out: &mut HashMap>,
) {
match command {
Command::Simple(simple) => {
// Skip `local` assignments
if simple.words.first().is_some_and(|w| w == "local") {
return;
}
if let Some(first_word) = simple.words.first() { // <-- BUG 🔴 Only checks the first word of the command
if let Some((name, value)) = first_word.split_once('=') {
if is_identifier(name) {
let rhs_type = classify_assignment_rhs(value);
let entry = out.entry(name.to_string()).or_insert(Some(rhs_type));
if let Some(existing) = *entry {
*entry = existing.merge(rhs_type);
}
}
}
}
}
// ...
```
# Evidence
### Multiple assignments on one line are missed
For the following Bash script:
```bash
foo() {
a=1 b=2
}
foo
echo $a
echo $b
```
The analysis correctly identifies `a` as a non-local assignment (because it's the first word), but completely misses `b`. As a result, `a` is declared as a global variable while `b` is treated as a local variable inside `foo`, breaking the script's logic.
**Actual Generated Amber:**
```amber
let a = 0
fun foo() {
trust $ a=1 b=2 $
}
foo()
echo(a)
trust $ echo \$b $
```
Note that `let a = 0` is present but `let b = 0` is missing. Also, `echo $b` failed to convert to a native Amber call because `b` was not recognized as a declared variable.
### `export` assignments are missed
For the following Bash script:
```bash
foo() {
export c=3
}
foo
echo $c
```
The analysis sees `export` as the first word and, since it doesn't contain an `=`, it misses the assignment to `c` entirely.
**Actual Generated Amber:**
```amber
fun foo() {
trust $ export c=3 $
}
foo()
trust $ echo \$c $
```
Again, `let c = 0` is missing from the global scope.
# Why has this bug gone undetected?
The bug likely went undetected because many simple scripts use one assignment per line. When multiple assignments are used, or when `export` is used, the generated code often falls back to `trust` blocks (as seen in the evidence), which might still "work" at runtime by executing raw shell commands, but fails to produce native Amber code and ignores the variables in subsequent Amber analysis.
# Recommended fix
Iterate through the words of the `SimpleCommand` and process all leading assignments. Also, handle builtins like `export`, `declare`, and `readonly` by skipping the builtin name and processing the subsequent assignments.
```rust
Command::Simple(simple) => {
let mut words = simple.words.iter();
let Some(first) = words.next() else { return };
if first == "local" {
return;
}
let start_words = if matches!(first.as_str(), "export" | "declare" | "readonly" | "typeset") {
words
} else {
simple.words.iter()
};
for word in start_words {
if let Some((name, value)) = word.split_once('=') {
if is_identifier(name) {
// ... process assignment ...
}
} else {
// Stop at the first word that isn't an assignment
break;
}
}
}
```
# Related bugs
The `commands_reference_variable` function in the same file also has a bug where it uses `word.contains(dollar_var)`, leading to false positives when one variable name is a prefix of another (e.g., `$a` matches in `$abc`).
# History
This bug was introduced in commit 7e8fbe8 (@Ph0enixKM, 2026-02-09). This commit refactored the rendering logic into separate modules and introduced the analysis module to track non-local variable assignments, but it inadvertently limited the assignment check to only the first word of a command and used imprecise string containment for variable references.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Évaluation
Cette issue n'a pas encore été évaluée.