Lukas/stdlib/fixes#761
Open
garazdawi wants to merge 41 commits into
Open
Conversation
parse_pax_time/1 was returning Micro div 1_000_000 - Mega*1_000_000, which is total_seconds rem 1_000_000, corrupting every PAX mtime/atime/ctime so extracted files were stamped near epoch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ?NAMECHAR macro in erl_scan.erl combined its two Latin-1 ranges
with andalso where orelse was intended:
C >= $ß andalso C =< $ÿ andalso C =/= $÷ andalso
C >= $À andalso C =< $Þ andalso C =/= $×
Because andalso binds tighter than orelse, the whole Latin-1 branch
became a single conjunction that is only satisfied by C in
[$ß..$ÿ] intersect [$À..$Þ] = [223..222] = the empty set. The macro
therefore never reported a Latin-1 letter as a name character.
?NAMECHAR is used as the post-number "trap" guard in scan_number,
scan_based_num, scan_based_fraction, scan_fraction and scan_exponent,
so input like "42é", "1.0À" or "16#FFFé" silently tokenised as a
number followed by a separate variable/atom token instead of producing
{illegal,integer} / {illegal,float} at scan time, deferring the error
to the parser at best.
Parenthesise the two Latin-1 sub-ranges and connect them with orelse,
mirroring the separate clauses already used in scan_name and scan1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8e3ab2b to
219dd3c
Compare
log_mf_h wrote each event as a 16-bit length prefix followed by the term_to_binary payload. put_int16/1 masked the size with 16#ff00/16#ff, so an event of 65536 bytes or more silently wrote a truncated header while still emitting the full payload. The next reader then consumed the wrong number of bytes, desynchronised, and treated the rest of the file as corrupted records. Use a backward-compatible sentinel: a 16-bit length of 0 marks an extended record whose real length is stored in the next 32-bit big-endian integer. Small events (< 64 KiB) keep their original two-byte header, so legacy log files (which never legitimately contained large events because of the bug) remain readable. A 16-bit prefix of zero never occurred in the legacy format because term_to_binary/1 always emits at least one byte. rb:read_report/1 is updated to recognise the sentinel so the documented reader stays in sync with the writer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lause
The clause handling anonymous-module native-record updates
(`Expr#_{...}`) in erl_expand_records:expr/2 threaded the compiler
state incorrectly: it processed the `Updates' list against `St0'
instead of `St1' (the state returned from processing `Arg0'), and
then re-matched the result against the already-bound `St1' instead
of binding a fresh `St2'.
Whenever `Arg0' or any of the `Updates' allocated a fresh
variable (e.g. a nested tuple-record update bumping the
`vcount'), the second match would `badmatch' and crash the
expand_records pass with an internal compiler error. The bug was
previously latent because the common case (Arg0 = variable,
Updates = literals) leaves the state unchanged.
Fix the threading to `St0 -> St1 -> St2', mirroring the sibling
clause that handles `Expr#mod:rec{...}'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `extra` branch of `epp:parse_file/2` (the {ok,Epp,Extra} clause)
sent `{get_features, self()}` to the Epp server and then waited for
`{features, X}` in an unguarded `receive`. If the Epp server had
terminated unexpectedly between sending the previous reply and
processing the `get_features` request, that `receive` would block
forever.
Extract the send/receive into a small `get_features/1` helper that
monitors the Epp server (mirroring the pattern used by
`wait_epp_reply/2`) so a `DOWN` message unblocks the caller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dets_server:pending_call/7 used a plain spawn/1 to run the actual
dets:add_user/internal_open/internal_close/remove_user call. If that
worker crashed (e.g. it was killed, or the dets process had just died
and gen_server:call raised) it would exit before sending the
{pending_reply, _} message, and the matching #pending{} entry would
stay in state.pending forever. Every subsequent open/close request for
the same Tab is then funneled into check_pending/4 and appended to
Clients, which is never drained — so the table becomes permanently
unreachable and the entries pile up unboundedly.
Use spawn_monitor/1, remember the monitor reference in the
#pending{} record, and handle the worker's 'DOWN' message: reply to
the original caller (and re-drive any queued clients) with an error,
and undo the speculative ?REGISTRY/?OWNERS rows that
do_internal_open/3 had pre-inserted. On the happy path the monitor is
demonitored (and flushed) when the pending_reply arrives.
Adds dets_SUITE:pending_worker_crash/1, which traces dets_server and
kills the pending_call worker before it can reply, then asserts that
a second dets:open_file/2 for the same table still returns instead of
hanging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The guards in form_urlencode/2 were written as
`is_list(Cs), Encoding =:= utf8; Encoding =:= unicode`, which Erlang
parses as `(is_list(Cs) AND utf8) OR unicode`. As a result, whenever the
caller requested `unicode` encoding, the list-input clause matched for
ANY input (atoms, integers, ...) and crashed inside convert_to_binary/3
with badarg instead of returning the documented
`{error, invalid_input, Cs}` tuple via the catch-all clause.
Parenthesise the disjunction so the type check actually gates the
clause, and add a regression test that exercises compose_query/2 with
atoms/integers and `{encoding, unicode}`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The guard in `find_eocd/1` that decides whether an archive needs to be treated as ZIP64 used `,` (AND) instead of `;` (OR) between the `EntriesOnDisk =:= ?MAX_INT16` and `Entries =:= ?MAX_INT16` checks. Because Erlang's `;`-separated guard list contains these two as one AND-joined alternative, the ZIP64 path was taken only when *both* fields were 0xFFFF. The PKZip spec sets either field to 0xFFFF as a "real value lives in the ZIP64 record" sentinel, and conforming producers usually set both together — which is why this almost never fires. A producer that sets only one (e.g., `EntriesOnDisk = 0xFFFF`, `Entries = 1`, or vice versa) would be mis-routed past the ZIP64 path to the standalone EOCD parse, which itself returns `none` for that record, and `list_dir` / `unzip` would then throw `bad_eocd` instead of using the real entry count from the ZIP64 EOCD. Change the stray `,` to `;` so either field hitting MAX_INT16 routes to ZIP64, matching the second `find_eocd/1` clause's guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In extract_type_specs/1, the AST-fold helper filter_exported_types/2 matched callback attributes against the `callback' map but wrote the update back under the `function' key. The effect was twofold: callback specs were never visible to render_signature/2 (the `maps:find/2' lookup keyed on `callback' always missed), and any -spec entries in the same module were silently clobbered as soon as a -callback was encountered. Write the updated map back under the `callback' key. Add a regression test that calls extract_type_specs/1 on gen_server and asserts that callback specs land in the callback map while the function map only contains -spec attributes, plus an end-to-end test that renders a gen_server callback via render_callback/4 and verifies that the AST "-callback" prefix appears in the rendered output (which only happens when render_signature/2 finds the spec in Specs.callback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recursive case in do_i_utf32_big_chk/1 and do_i_utf32_little_chk/1
only matched {error,_,_} and lists, so a partial UTF-32 codepoint at
the tail of a binary that was otherwise representable as latin1 caused
a case_clause crash. The surrounding try mapped it to badarg instead
of the documented {incomplete,...} tuple. The corresponding utf16
variants already handle the incomplete branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fast-path clauses of decompose/1 and decompose_compat/1 in unicode_util.erl guarded with `is_integer(CP), CP < 16#AC00, 16#D7A3 > CP`. The intent was to exclude Hangul Syllables (16#AC00..16#D7A3) on both sides, but the second conjunct is `CP < 16#D7A3` instead of `CP > 16#D7A3`, so it is subsumed by the first and every code point above 16#D7A3 falls through to the slow `canonical_order(decompose_1(CP))` path, even when the code point has no decomposition. That covers all of CJK Ext A-G, the Private Use Area, all CJK Compatibility Ideographs, every CJK Ideograph in the SIP, and so on -- the majority of non-BMP normalization work. Output is identical on both paths, so this is a pure performance bug. Fix it in lib/stdlib/uc_spec/gen_unicode_mod.escript (which emits unicode_util.erl) by splitting the guard into two clauses joined by `;`: `CP < 16#AC00; ... CP > 16#D7A3`. Regenerated unicode_util.erl is gitignored. The new decompose_fast_path test in unicode_util_SUITE traces the internal decompose_1/1 and decompose_compat_1/1 helpers while exercising nfd/nfkd on code points above 16#D7A3 (U+E000, U+F900, U+20000) and asserts the helpers are only called for the Hangul block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gen:format_status/4 compared the size of the merged map against the
size of the callback's NewStatus instead of the input Status. Because
maps:merge(Status, NewStatus) yields a map whose key set is the union
of both, equality with size(NewStatus) holds exactly when the callback
returned a *superset* of the input keys - the opposite of what the
error message ("returned a map with unknown keys") describes.
As a result:
* A callback returning a strict subset of the input - the
documented natural pattern (e.g. #{state => Sanitized}) - hit
the false branch and got an 'EXIT' "unknown keys" string
substituted for the user's sanitized state.
* A callback returning genuinely unknown extra keys passed the
check silently and propagated those extras to sys:get_status.
Compare against maps:size(Status) instead, so equality holds iff
keys(NewStatus) is a subset of keys(Status). Extras now correctly
trigger the EXIT error, and the documented subset pattern works.
Add a gen_server_SUITE test case (format_status_subset) covering a
format_status/1 callback that returns #{state => Sanitized} and
asserts the sanitized value reaches sys:get_status untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
do_swap/7 invokes the swapped-in handler's init/1 but only matched
{ok, State2a}. A documented return value of {ok, State2a, hibernate}
fell through to the Other clause and was treated as a crash: the swap
was aborted, the handler dropped and a crash report logged.
Add the {ok, State2a, hibernate} clause and return {hibernate, Handler}.
server_update/4, server_call_update/3, server_notify/4 and server_call/4
already propagate the {hibernate, _} shape so the manager hibernates as
expected after the swap.
Regression test: gen_event_SUITE:swap_handler_hibernate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `none` clause of the non-empty `is_map` case in `write_bin1/6`
returned `{~"#{}", 3}` instead of appending `"#{}"` to the binary
accumulator. As a result, when an empty map appeared mid-term, any
text already accumulated before it was silently dropped — e.g.
`io_lib:bwrite([1, #{}, 2], [])` produced `<<"#{},2]">>` instead of
`<<"[1,#{},2]">>`.
Fix the clause to append to `Acc` and add `Sz`, matching the shape
of the sibling `D =:= 1` empty-map clause on the line above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build_limited_bin/5 only handled list-prefixed newlines via [[$\n|_]=NL|Cs]; a bare integer $\n in the chars-to-print stream fell through to the generic integer clause and incremented the column counter instead of resetting it to 0. This caused the binary format path (fwrite_bin/build_bin) to track the wrong indentation column whenever a literal "\n" appeared in the format string before a ~p/~P/~w/~W/~s, making the pretty printer wrap the following term at the wrong column. The list-based build_limited/5 already has the equivalent [$\n|Cs] clause; mirror it in build_limited_bin/5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
io_ansi:is_color/1 listed the non-existent atom background_color in its
set of color-related keys. The real mapping key for indexed/RGB
background (see lib/stdlib/src/io_ansi.erl ~line 2711) is background.
As a result, calling
io_ansi:format([{background, 4}, "x"], [],
[{enabled, true}, {color, false}])
still emitted the background SGR, contrary to the documented contract
of the {color, false} option. Replace background_color with background
so that {background, N} and {background, R, G, B} directives are
correctly suppressed when colors are disabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arborescence_root/1 (and the wrapping is_arborescence/1) checked only
that the edge count equaled no_vertices-1 and that exactly one vertex
had in-degree 0 -- conditions satisfied by graphs containing a
disconnected directed cycle (e.g. vertices [a,b,c,d] with edges
{a,b},{c,d},{d,c}). The candidate root could not reach every vertex,
so the result was not actually an arborescence. Verify reachability
from the root via reachable/2 before reporting success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
init_features/0 used `catch _ -> false` which only catches class
`throw`. Any class:error or class:exit raised from inside the try (for
example list_to_atom/1 raising error:system_limit on an overlong
`-enable-feature` argument) was therefore not swallowed by the
surrounding "Convert failure ... to not being a valid feature" intent,
and instead crashed the VM during boot ("Runtime terminating during
boot"). Change the catch to `catch _:_ -> false` so all error classes
are filtered, matching the surrounding comment.
features_in/1 used erlang:binary_to_term/1 on the "Meta" BEAM chunk
without [safe], allowing a hostile or corrupt beam to create new atoms
(or otherwise misbehave) on the runtime that called erl_features:used/1
to inspect its enabled features. Add [safe] so unknown atoms cause a
clean badarg instead of being silently interned.
Both fixes harden init/inspection paths against bad command-line or
on-disk input and ship together since they target the same hardening
review item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When pool_master received a {nodedown, Node} message it removed the
node via lists:keydelete/3, but any {Node, load, Load} messages already
in flight from that slave's statistic_collector still reached
handle_info and were passed to insert_node/2. With no matching entry
left, insert_node walked off the end of the list and hit the "Can't
happen" clause, which logged an error and called exit(crash). Since
every slave's statistic_collector is spawn_link'ed from pool_master,
crashing the master also killed load reporting on every slave node in
the pool. Drop the stale report in the [] clause instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
format_section/4 invoked format_function/3 once per doc via lists:flatmap,
so the lists:foldr grouping in format_function/3 only ever saw one element
and never merged entries that shared the same equiv metadata target. Any
module using -doc(#{equiv => ...}) ended up with duplicated, ungrouped
stanzas in its generated man page. Call format_function/3 once with the
whole sorted list so the grouping fold actually runs across docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed completions A comma instead of a semicolon in the to_legacy_format/1 guard at edlin_expand.erl:1079 made the sub-clause unsatisfiable (a string can never equal both "user_defined" and "records" at the same time). As a result, sections titled "user_defined" fell through to the catch-all clause and were emitted as the bare title string "user_defined" instead of being unwrapped into their elements -- silently dropping every user-defined function completion from legacy-mode shell output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
over_to_opening_paren/3's guard "Q == $\"; Q == $', NEC /= $$, NEC /= $\\"
parses as "(Q == $\") ; (Q == $' , NEC /= $$ , NEC /= $\\\\)" because ','
binds tighter than ';', so the escape checks were silently skipped for
double-quoted strings. Worse, the clause destructured [Q, NEC | Bef] and
called over_to_opening_quote(Q, Bef), consuming NEC without putting it
back -- so scanning back through ("ab") yielded ("a"), losing the char
immediately before the closing quote. Parenthesize the quote-character
disjunction and feed [NEC|Bef] into over_to_opening_quote.
A pre-existing test asserted {error,_} for the valid term {"", $"}; that
spurious error was a symptom of the same bug, so the assertion is
updated to the correct {tuple,...} result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edit/5's eof handler pulled the *first* line of LA as the target while
moving the cursor down length(LA) rows, so a multi-line buffer with the
cursor above other lines left the cursor mid-line on the wrong row when
EOF arrived. Pick the actual last line of LA instead.
The do_op({insert_search, C}, ...) clause for non-empty Aft returned a
3-tuple {MultiLine, Rs, search}, but the caller in edit/5 only matched
2-tuples (plus {blink,...}/{redraw,...}), so a search-mode keystroke
landing on a non-empty Aft raised case_clause. Route the search tag
back to the caller of edit_line/2 the same way the {key,_} branch
already does, so group.erl's {search, ...} handler can refresh the
result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dules
The intermediate-result cache in edlin_type_suggestion:type_traverser/5
keyed user_type and remote_type entries on strip_anno(T) alone. For
user_type the stripped form is {user_type, Name, Params}, which omits
the calling module. Two modules that happen to declare a local type with
the same name+arity (but different RHS ASTs) shared the same cache
entry, so the second tab-completion request received the first module's
expanded AST — typically with the wrong containing Mod baked in and the
wrong simplified type body.
The catch-all {type, _, Name, Params} clause cached under a hardcoded
Level=1 while the recursive calls passed the dynamic Level, so a first
query at depth N would poison a later query at depth M.
Both fixes are mechanical: include Mod in the user_type cache key, and
use the actual Level (rather than literal 1) in the catch-all clause.
remote_type already encodes Mod inside the AST tuple and is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the beam's compile_info had no `options' entry,
c:compile_info_add_cwd/2 returned `[{options, CwdOpts}]', discarding
every other entry of the original Info list, including `source'. As a
result, c:find_source/2 could not see the recorded source path and fell
back to filelib:find_source/1, so c:c(Module) on such a beam returned
`{error, no_source}' instead of recompiling. Preserve the remaining
entries by prepending the synthesized options entry to the input Info.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
filename:basename("foo//") returned [] while the binary path
filename:basename(<<"foo//">>) returned <<"foo">> -- two implementations
of the same operation disagreed on what to do with multiple trailing
directory separators. Likewise filename:basename(<<"foo.erl/">>, <<".erl">>)
returned <<"foo.erl">> (the extension was not stripped) because the
suffix match was done against the original binary, whose trailing "/"
made the .erl pattern miss; the list variant had the analogous bug via
a different code path. After this change both the list and binary
implementations strip all trailing separators first, then strip the
extension, giving "foo" in every variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In argparse, the choice-form string type {string, Choices} is guarded
by is_list(hd(Choices)), which fails for an empty Choices list. The
clause then fell through to the {string, Re} regex clause with Re=[],
and re:run/2 with an empty pattern matches anything at offset 0 — so
{string, []} silently accepted every input value instead of rejecting
all of them (the documented "no choices" behaviour, matching how
{atom, []} works via lists:member/2). Add an explicit {string, []}
clause that throws the "is not one of the choices" error, mirroring
the atom case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge chars The internal b64d/2 lookup did element(X+Off, Tuple512) on a tuple that holds the standard alphabet at positions 1..256 followed by the urlsafe alphabet at positions 257..512. With a list input in standard mode (Off=1) and a character X in 256..511, the lookup silently indexed into the urlsafe section instead of failing. For example, base64:decode([$A + 256, $B, $C, $D]) returned the same <<0,16,131>> as the well-formed [$A,$B,$C,$D] rather than raising. Binary inputs were unaffected because <<C:8, ...>> clamps to 0..255. Guard b64d/2 on 0 =< X =< 255 so any list-borne non-byte raises cleanly via the normal decode-error path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
beam_lib:open_file/1 read files in a 256 KB file:read/2 loop, building
an iolist that maybe_uncompress later flattened. Worse,
read_all_chunks/1 and read_all_but_useless_chunks/1 each ran the whole
open-and-read sequence twice (once with `info`, once with the resolved
chunk IDs), doubling the I/O and gunzip cost on every all_chunks/1,
cmp/2, cmp_dirs/2, and strip_release/1 call.
Switch open_file/1 to a single file:read_file/1, and add a scan_beam_bb
entry point that reuses the already-loaded #bb{} buffer across both
scans. Behaviour is preserved; only the number of underlying file
operations changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
filelib:fold_files/5,6 used to descend through directory symlinks
without any cycle detection, so a directory containing a symlink that
points back to one of its ancestors (e.g. a/b -> ../a) caused the
recursive walk to follow the link indefinitely. In practice the OS
eventually returned ELOOP/enametoolong once the synthetic path grew
past the kernel's SYMLOOP_MAX, but by then every matching file under
the cycle had already been reported many times (32+ on macOS), and
fold_files quietly returned a result list polluted with duplicate
entries via .../b/b/.../b/ paths.
Track each visited directory by its {major_device, inode} pair, falling
back to its canonical path on filesystems that don't expose stable
inode numbers, and skip directories already on the descent path. Adds
a regression test that builds an a/b -> ../a cycle and asserts the
contained .txt file is reported exactly once, with a timeout guard so
a future re-introduction of the bug fails loudly instead of hanging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
one_path/8 tracked the DFS visited set as a plain list and tested membership with lists:member/2, making each step linear in the path length so far and the whole traversal O(V^2). On long paths get_path/get_cycle (and acyclic_add_edge, which calls get_path on every edge insertion via add_edge) spent quadratic time scanning already-visited vertices. Replace the visited list with a map and swap lists:member/2 for is_map_key/2 so each lookup is O(1). The returned path is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ets:fun2ms/1, when called from the shell, used to pattern-match the
{error, [_|_], _Warnings} tuple produced by
ms_transform:transform_from_shell/3 and replace it with the opaque
{error, transform_error} value, discarding the location, module and
reason carried by the parser diagnostics. This made fun2ms - a
debugging helper - silently useless when the user passed a fun that
ms_transform cannot translate: callers got "transform_error" with no
hint about which clause, which expression or what was wrong.
Return the underlying {error, Errors, Warnings} tuple verbatim so that
callers can inspect the diagnostics or pretty-print them with
ms_transform:format_error/1. Update the -spec to reflect the new
return type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IS_NUMBERED macro was defined as `is_integer(X) andalso min(0, X) =:= 0`, which is equivalent to `X >= 0` and so accepted every possible byte read from a binary. As a result the Markdown parser treated any single non-whitespace byte followed by ". " as the start of an ordered list, so paragraphs such as `x. apple` or `_. apple` rendered as <ol> items instead of plain paragraphs. Redefine the macro to actually check the `$0`..`$9` range so only real numeric markers start a numbered list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
precomp_repl/1 in re.erl parsed the replacement binary with a
non-tail-recursive walk that constructed each literal chunk by
prepending bytes with <<X, BHead/binary>>. That pattern reallocates the
whole binary at every step (BEAM's binary-builder optimisation only
fires for appends), so a replacement string of length N took O(N^2)
time. A 32 KiB literal-heavy replacement was already ~150 ms; large
replacements were effectively unusable.
Rewrite the helper as a tail-recursive accumulator that appends bytes
into a buffer with <<Buf/binary, X>> and flushes the buffer onto the
output list when a \N / & / \g{...} backreference is reached. The
output list shape (and the "escaped character merges into the
surrounding binary chunk" behaviour relied on by do_replace/3) is
preserved. Replacement parsing is now linear: on the test host the
32x-input run drops from ~1100x time to ~21x.
The new regression test (replace_long_replacement_linear) measures
re:replace/4 on small vs. large literal replacements and asserts a
linear-ish ratio (<60x for a 16x size increase), well under what the
old quadratic code produced (~100-250x).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
spec_decl/4 unconditionally stored the current -spec attribute's Anno in the specs map before checking whether the MFA was already present, so a redefined spec overwrote the original spec's Anno. Downstream diagnostics that look up the stored Anno later (notably spec_fun_undefined emitted by check_specs_without_function/1) then pointed at the last redefinition rather than at the original -spec, sending the user to the wrong source location. Only insert into the specs map in the non-redefinition branch so the first -spec's Anno is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The body of legalize_vars/1 invoked ?TEST(F) before F was bound. In production builds ?TEST/1 expands to ok so the bug was latent, but any DEBUG build of erl_pp (-define(DEBUG, true)) failed to compile with unbound_var / unsafe_var errors against F. Pass the input form pattern to ?TEST/1 instead, matching the convention used by the other public entry points, and add a regression test that compiles erl_pp.erl itself with DEBUG=true. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eval_zip called check_bad_generators on every iteration of a zipped
comprehension, and the map-generator clause rebuilt the entire
remaining iterator into a fresh map via #{K => V || K := V <- Iter0}
just so is_generator_end/1 could compare it against #{}. With an
N-element map generator that made every step O(N), so the zip as a
whole was O(N^2).
Tag each generator value with its origin (list/binary/map iterator),
keep map iterators in their raw form during the check, and only
materialise them when we actually need to embed the remaining
generators in a {bad_generators,_} error term. Behaviour and error
shape are unchanged; performance for zips over map generators is now
linear in the map size.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The io_server_loop in peer.erl handled 'DOWN' messages by
unconditionally calling maps:take/2 to look up the monitor reference
in its outstanding-call table. A stray 'DOWN' (e.g. one referencing a
monitor set up elsewhere on the peer node) caused maps:take/2 to
return error, the {Seq, Refs3} = ... pattern match to fail, the
io_server to crash, and the surrounding try/catch in io_server/0 (and
tcp_init/2) to halt the entire peer node. Match the result of
maps:take/2 instead and silently ignore unknown references.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
erl_error:pp_arguments/5 assumed the formatter callback always returned
output wrapped in `[...]' (the shape produced by io_lib:print/4), and
unconditionally stripped one byte from each end via a binary match in
brackets_to_parens/2. A user-supplied `format_fun' option to
erl_error:format_exception/4 (and the internal-but-exported
format_call/4,5) is allowed to return any chardata, e.g. a truncation
marker like "...". Such a return value crashed the whole exception
formatter with {badmatch, ...} deep in brackets_to_parens/2, making the
custom-formatter path effectively unusable for anything that wasn't
already a list-shaped chardata.
Have brackets_to_parens/2 signal `error' when the formatter output is
not bracket-wrapped, and have pp_arguments/5 fall back to the
format_fun's plain output in that case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
acyclic_add_edge/4 in graph.erl ran two left-over debug asserts (false = has_path(G, To, From) on the success branch and true = has_path(G, To, From) on the cycle branch, both marked "assert - remove me") on top of the get_path/3 call that already implements the cycle check. Each call to add_edge/3,4 on an acyclic graph therefore performed the cycle DFS twice, roughly doubling the cost of every edge insert. Remove the asserts. A trace-based regression test (acyclic_add_edge_no_has_path) patterns has_path/3 with a local tracer and asserts add_edge/3 on an acyclic graph does not invoke has_path/3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound Two recurring developer trip-ups that weren't covered: * TEST_NEEDS_RELEASE=true is the default for many apps' test targets, so `make stdlib_test ARGS=...` releases OTP into a temp directory on every invocation. Document the false override for iterative runs. * When the system Erlang on PATH is older than the previous major release, the bootstrap step fails with confusing "unknown function" errors in stdlib sources that use newer language features. Document the PATH=<recent-otp>/bin:$PATH workaround. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219dd3c to
e76cce0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.