Skip to content
Merged
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
20 changes: 11 additions & 9 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -4952,6 +4952,9 @@ static Value builtin_split(Interpreter* interp, Value* args, int argc, Expr** ar
if (argc >= 2) {
EXPECT_STR(args[1], "SPLIT", interp, line, col);
sep = args[1].as.s;
if (sep[0] == '\0') {
RUNTIME_ERROR(interp, "SPLIT expects a non-empty delimiter", line, col);
}
}
const char* s = args[0].as.s;
// simple separator: if sep==NULL split on whitespace, else split on sep exactly
Expand Down Expand Up @@ -4984,16 +4987,15 @@ static Value builtin_split(Interpreter* interp, Value* args, int argc, Expr** ar
free(piece);
cur = found + seplen;
}
// last piece
if (*cur != '\0') {
if (count + 1 > cap) { cap *= 2; items = realloc(items, sizeof(Value) * cap); }
items[count++] = value_str(cur);
}
// last piece, including empty trailing fields
size_t len = strlen(cur);
char* piece = malloc(len + 1);
Comment on lines 4955 to +4992
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This change introduces new SPLIT behaviors that should be covered by the existing builtin test suite (tests/test2.pre): (1) empty delimiter now raises a runtime error, and (2) empty trailing fields are now preserved (e.g., splitting "a," by "," should include a final empty string). Adding explicit assertions will prevent regressions and clarify intended semantics (including the "" input case).

Copilot uses AI. Check for mistakes.
memcpy(piece, cur, len);
piece[len] = '\0';
if (count + 1 > cap) { cap *= 2; items = realloc(items, sizeof(Value) * cap); }
items[count++] = value_str(piece);
free(piece);
Comment on lines +4991 to +4997
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The newly added last-piece handling allocates piece with malloc(len + 1) but never checks for allocation failure before memcpy, which can dereference NULL on OOM. Since value_str() already duplicates its input, you can avoid this allocation entirely by passing cur directly; otherwise add an explicit NULL check and clean up (free(copy), free(items)) before raising the runtime error.

Suggested change
size_t len = strlen(cur);
char* piece = malloc(len + 1);
memcpy(piece, cur, len);
piece[len] = '\0';
if (count + 1 > cap) { cap *= 2; items = realloc(items, sizeof(Value) * cap); }
items[count++] = value_str(piece);
free(piece);
if (count + 1 > cap) { cap *= 2; items = realloc(items, sizeof(Value) * cap); }
items[count++] = value_str(cur);

Copilot uses AI. Check for mistakes.
Comment on lines +4990 to +4997
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The PR title mentions fixing empty-delimiter handling, but this hunk also changes SPLIT semantics by always appending the final field (including empty trailing fields, and changing the result for an empty input string). If that broader behavior change is intentional, consider reflecting it in the PR description/title and/or documenting it in the SPLIT comment/spec so the change is discoverable for users.

Copilot uses AI. Check for mistakes.
free(copy);
if (count == 0) {
free(items);
return value_tns_new(TYPE_STR, 1, (const size_t[]){0});
}
size_t shape[1] = { count };
Value out = value_tns_from_values(TYPE_STR, 1, shape, items, count);
for (size_t i = 0; i < count; i++) value_free(items[i]);
Expand Down
Loading