Skip to content

Comments

feat: add clap-style arg parser#3206

Closed
peter-jerry-ye wants to merge 19 commits intomainfrom
zihang/add-clap
Closed

feat: add clap-style arg parser#3206
peter-jerry-ye wants to merge 19 commits intomainfrom
zihang/add-clap

Conversation

@peter-jerry-ye
Copy link
Collaborator

@peter-jerry-ye peter-jerry-ye commented Feb 5, 2026

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +104 to +127
while (*env != NULL) {
// Find the '=' character in the environment variable string
char *equals = strchr(*env, '=');
if (equals != NULL) {
// Calculate lengths of key and value
size_t key_len = equals - *env;
size_t val_len = strlen(equals + 1);

// Create bytes object for key and copy data
moonbit_bytes_t key = moonbit_make_bytes(key_len, 0);
memcpy(key, *env, key_len);

// Create bytes object for value and copy data
moonbit_bytes_t value = moonbit_make_bytes(val_len, 0);
memcpy(value, equals + 1, val_len);

// Store key and value in result array
// Even indices store keys, odd indices store values
result[i * 2] = key;
result[i * 2 + 1] = value;
}
env++;
i++;
}

Choose a reason for hiding this comment

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

🔴 Uninitialized array elements in get_env_vars() when environment variable lacks '=' separator

In the native C stub for get_env_vars(), the code allocates an array of size count * 2 based on the total count of environment variables, then iterates through and only populates array elements when an environment variable contains an = character. If an environment variable lacks this separator (edge case), the corresponding array slots remain uninitialized.

Root Cause

In native_stub.c:104-127 (Unix) and lines 67-84 (Windows), the loop increments i regardless of whether the equals pointer is NULL:

while (*env != NULL) {
    char *equals = strchr(*env, '=');
    if (equals != NULL) {
        // Only populate result[i * 2] and result[i * 2 + 1] here
    }
    env++;
    i++;  // Always incremented, even if equals == NULL
}

The MoonBit code in env_native.mbt:50-54 then iterates through the entire array length assuming all elements are valid:

for i = 0; i < tmp.length(); i = i + 2 {
    let key = utf8_bytes_to_mbt_string(tmp[i])  // May access uninitialized memory
    let value = utf8_bytes_to_mbt_string(tmp[i + 1])
    res[key] = value
}

Impact: Could cause undefined behavior or crashes when accessing uninitialized array elements, though this edge case is rare in practice since environment variables normally always contain =.

Prompt for agents
Fix the get_env_vars() function in env/native_stub.c to handle environment variables that lack an '=' separator. Two approaches: (1) Only count variables that have '=' in the first pass, so the array size matches actual populated entries, or (2) Skip incrementing 'i' when equals is NULL. The same fix needs to be applied to both the Unix code (lines 92-128) and Windows code (lines 46-87). For example, in the Unix section, change the loop to only increment 'i' inside the 'if (equals != NULL)' block, not after it.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coveralls
Copy link
Collaborator

coveralls commented Feb 5, 2026

Pull Request Test Coverage Report for Build 2565

Details

  • 808 of 884 (91.4%) changed or added relevant lines in 7 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.3%) to 95.635%

Changes Missing Coverage Covered Lines Changed/Added Lines %
arg_parser/clap_matches.mbt 27 29 93.1%
env/env.mbt 1 5 20.0%
arg_parser/clap_group.mbt 12 17 70.59%
arg_parser/clap_arg.mbt 55 61 90.16%
arg_parser/clap_help.mbt 161 167 96.41%
env/env_wasm.mbt 4 16 25.0%
arg_parser/clap_command.mbt 548 589 93.04%
Totals Coverage Status
Change from base Build 2548: -0.3%
Covered Lines: 12708
Relevant Lines: 13288

💛 - Coveralls

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

let with_index = []
let without_index = []
for arg in args {
if arg.long is None && arg.short is None {

Choose a reason for hiding this comment

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

🟡 Inconsistent positional argument detection in positional_args vs is_positional_arg

The positional_args function only checks arg.long is None && arg.short is None to identify positional arguments, but is_positional_arg also checks that all alias arrays are empty. This inconsistency means an argument with no short/long but with aliases would be incorrectly treated as a positional argument by positional_args, causing it to be assigned positional values when it shouldn't be.

Root Cause

At arg_parser/clap_command.mbt:1235, the check is:

if arg.long is None && arg.short is None {

But at arg_parser/clap_command.mbt:585-591, is_positional_arg checks:

arg.short is None &&
arg.long is None &&
arg.aliases.length() == 0 &&
arg.short_aliases.length() == 0 &&
arg.visible_aliases.length() == 0 &&
arg.visible_short_aliases.length() == 0

Impact: If a user creates an Arg with only aliases (no short/long) but intends it to be a named option via those aliases, positional_args would incorrectly include it in the positional arguments list.

Suggested change
if arg.long is None && arg.short is None {
if is_positional_arg(arg) {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@peter-jerry-ye peter-jerry-ye marked this pull request as draft February 5, 2026 06:38
@peter-jerry-ye peter-jerry-ye marked this pull request as ready for review February 6, 2026 08:07
.about("demo")
.arg(@arg_parser.Arg::new("count").long("count").option())
.arg(@arg_parser.Arg::new("name").index(0))
let mut text = ""
Copy link
Contributor

@bobzhang bobzhang Feb 6, 2026

Choose a reason for hiding this comment

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

Command(name="demo",about="demo", args = [
OptionalArg("count", long = "count", optional = true),
PositionalArg("name", index = 0)
])

@peter-jerry-ye peter-jerry-ye marked this pull request as draft February 6, 2026 09:04
@peter-jerry-ye
Copy link
Collaborator Author

Continue in #3213. The env will be added in another pr.

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.

3 participants