Quick Overview

This question evaluates parsing and string-processing skills, hierarchical data structure design for call-tree reconstruction, exception trace analysis, and algorithmic complexity reasoning for robust log handling.

Parse and Reconstruct Stack Trace

Company: Anthropic

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

Given a multi-line stack trace string from a single thread (e.g., each frame is in the form 'at Module::Function(file:line)'), design and implement a parser that: ( 1) extracts the ordered list of frames from most recent to oldest; ( 2) reconstructs the call hierarchy as a tree; and ( 3) identifies the deepest frame associated with the thrown exception. The parser must correctly handle nested causes (e.g., 'Caused by:' sections), abbreviated repeated frames (e.g., '... N more'), and missing or malformed lines. Describe your data structures, time/space complexity, and provide working code for parse and reconstruction.

Overview: This question evaluates parsing and string-processing skills, hierarchical data structure design for call-tree reconstruction, exception trace analysis, and algorithmic complexity reasoning for robust log handling.

Part 1: Extract Frames from the Outermost Stack Trace Section

Extract the valid stack-frame lines from the **outermost (first) exception section** of a multi-line stack trace. ## Function ```python def solution(trace): ... ``` - **Input:** `trace` — a single string containing the whole stack trace, with lines separated by newlines. It may be empty. - **Output:** a **list of strings**, one per valid frame, in the same order they appear in the input (**most recent call first, oldest call last**). ## Line format Read the trace line by line. Before testing a line, **trim leading/trailing whitespace**. A line is a **valid frame** if, after trimming, it has the form: ``` at Module::Function(file:line) ``` Concretely, a trimmed line qualifies only when **all** of the following hold: - It starts with the prefix `at ` (the word `at` followed by a space). - Let **body** be the text after that prefix, with its own surrounding whitespace trimmed (e.g. `Module::Function(file:line)`). - **body** ends with a closing parenthesis `)`. - **body** contains an opening parenthesis `(` and, after it, a colon `:` (use the **last** `(` and the **last** `:`); the `(` must come before the `:`, and the `:` must not be the final character (so a line number follows it). - The text between that `:` and the closing `)` is the **line number**: it must be **non-empty and consist only of digits** (a non-negative integer). - The text before the `(` (the `Module::Function` part) must be **non-empty**. ## What to return for each frame For a valid frame, append the **body** — that is, the frame **without** the leading `at ` prefix and with surrounding whitespace removed (e.g. `App::run(app.py:10)`). Do not include `at ` in the output. ## Section boundary Scan the lines top to bottom. As soon as you reach a line whose **trimmed** form starts with `Caused by:`, **stop immediately** — that line and everything after it belongs to a nested cause and must be ignored. Only the first (outermost) exception section is parsed. ## Other rules - **Skip** any line that is blank, malformed, or does not match the valid-frame format (e.g. header lines like `RuntimeError: boom` or `random text`, or a frame whose body has no `(...:...)` shape such as `at BadLine(nope)`). - If `trace` is empty, or if a `Caused by:` line appears before any valid frame, return an **empty list**. - Preserve the original order of the valid frames you keep (most recent to oldest). ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most `10000` lines. - A valid frame line must start with `at ` after trimming and must end with `(file:line)`, where `line` is a non-negative integer. - Only the first exception section is parsed; later `Caused by:` sections are ignored. ## Examples **Input** ``` RuntimeError: boom at App::run(app.py:10) at Main::main(main.py:20) ``` **Output:** `['App::run(app.py:10)', 'Main::main(main.py:20)']` --- **Input** ``` ValueError random text at Parser::parse(parser.py:7) at BadLine(nope) Caused by: IOError at IO::read(io.py:3) ``` **Output:** `['Parser::parse(parser.py:7)']` (`ValueError` and `random text` are not frames; `at BadLine(nope)` has no `:` after its `(`, so it fails the `(file:line)` format; parsing stops at `Caused by:`, so `at IO::read(io.py:3)` is ignored.) --- **Input** ``` TopError Caused by: Inner at X::f(x.py:1) ``` **Output:** `[]` (the `Caused by:` line ends the section before any frame is collected.)

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.
  • Only the first exception section should be parsed; later 'Caused by:' sections are ignored.

Examples

Input: 'RuntimeError: boom\nat App::run(app.py:10)\nat Main::main(main.py:20)'

Expected Output: ['App::run(app.py:10)', 'Main::main(main.py:20)']

Explanation: Both frame lines are valid and belong to the outermost section.

Input: 'ValueError\nrandom text\n at Parser::parse(parser.py:7)\nat BadLine(nope)\nCaused by: IOError\nat IO::read(io.py:3)'

Expected Output: ['Parser::parse(parser.py:7)']

Explanation: The malformed line is skipped, and parsing stops before the cause section.

Hints

  1. Process the trace line by line and stop at the first 'Caused by:' line.
  2. You do not need a full parser; checking the position of '(', ':', and ')' is enough to validate a frame.

Part 2: Reconstruct a Merged Call Hierarchy Tree

Reconstruct the merged call hierarchy of a single thread's stack trace and return it as a **forest of nested dictionaries**. ## Function ```python def solution(trace): ... ``` `trace` is a single string containing the full multi-line stack trace (lines separated by `\n`). Return a nested dictionary of the form `{frame: child_subtree}` (see **Output** below). ## Background A stack trace may contain several **exception sections** chained by `Caused by:` lines, and a section may abbreviate its repeated tail using a `... N more` line. Your job is to rebuild every section's full call stack and merge them all into one tree. ## Frame lines After trimming surrounding whitespace, a **valid frame line** has the form: ``` at Module::Function(file:line) ``` A line is a valid frame **only if** the trimmed line: - starts with `at ` (the literal `at` followed by a space), and - ends with `)`, and - contains an opening `(` that comes before the final `:`, and - has a non-empty part before that `(`, and - has a **line number** (the text between the final `:` and the closing `)`) consisting only of digits (a non-negative integer). The **frame** you record is the part of the line **after** `at ` (for example, the line `at B::b(b.py:2)` yields the frame `B::b(b.py:2)`). Lines that do not satisfy every rule above are **malformed** and must be ignored. ## Sections - The **first section** begins at the start of the trace. - Each line whose trimmed form starts with `Caused by:` ends the current section and begins a **new section** (the `Caused by:` line itself is not a frame). - Within a section, valid frame lines are collected **in the order they appear**, which is most recent call first down to oldest call. ## The `... N more` abbreviation A line whose trimmed form starts with `... ` and ends with ` more`, with a digit-only number `N` in between, is an **abbreviation line**. Each section contains **at most one** such line. When a section (other than the first) has an abbreviation `... N more`, append the **last N frames of the previous section's fully reconstructed stack** to this section's collected frames: - "Previous" means the section immediately before this one, **after** that section has itself been fully reconstructed (so abbreviations chain transitively through `Caused by:` sections). - If `N` is larger than the number of frames in the previous reconstructed stack, append **all** of them. - An abbreviation line in the first section (which has no previous section) contributes nothing. ## Output Insert every fully reconstructed stack into a single tree, walking each stack from its **oldest call to its most recent call**. The tree is represented by nested dictionaries: each frame maps to the dictionary of its child frames, e.g. `{frame: {childFrame: {...}}}`. A leaf frame maps to an empty dictionary `{}`. - Stacks that share a common prefix of oldest calls share those nodes in the tree. - If multiple disconnected roots exist, they all appear at the **top level** of the returned dictionary. - An empty reconstructed stack (for example, a section with only malformed lines) contributes nothing. ## Edge cases - An empty `trace` returns `{}`. - Malformed lines, blank lines, and exception-description lines are silently skipped. ## Example Input: ``` Outer at Service::run(s.py:30) at Controller::handle(c.py:20) at Main::main(m.py:10) Caused by: ParseError at Parser::parse(p.py:5) ... 2 more ``` The first section's stack (most recent → oldest) is `Service::run`, `Controller::handle`, `Main::main`. The second section starts with `Parser::parse`, then `... 2 more` appends the last 2 frames of the previous reconstructed stack (`Controller::handle`, `Main::main`), giving `Parser::parse`, `Controller::handle`, `Main::main`. Inserting both stacks oldest-first and merging the shared `Main::main → Controller::handle` prefix produces: ```python {"Main::main(m.py:10)": {"Controller::handle(c.py:20)": {"Service::run(s.py:30)": {}, "Parser::parse(p.py:5)": {}}}} ``` ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most 10000 lines. - Each section contains at most one abbreviation line of the form `... N more`. - A valid frame line must start with `at ` after trimming and must end with `(file:line)` where `line` is a non-negative integer.

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • Each section contains at most one abbreviation line of the form '... N more'.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.

Examples

Input: 'Err\nat B::b(b.py:2)\nat A::a(a.py:1)'

Expected Output: {'A::a(a.py:1)': {'B::b(b.py:2)': {}}}

Explanation: A single stack becomes a single root-to-leaf path from oldest to newest.

Input: 'Outer\nat Service::run(s.py:30)\nat Controller::handle(c.py:20)\nat Main::main(m.py:10)\nCaused by: ParseError\nat Parser::parse(p.py:5)\n... 2 more'

Expected Output: {'Main::main(m.py:10)': {'Controller::handle(c.py:20)': {'Service::run(s.py:30)': {}, 'Parser::parse(p.py:5)': {}}}}

Explanation: The cause expands to [Parser::parse, Controller::handle, Main::main], creating a branch under Controller::handle.

Approach

The solution reconstructs each exception section's stack independently, resolving ... N more abbreviations against the previous reconstructed stack, then merges all stacks into a nested-dict forest. Parsing helpers. - parse_frame validates a line of the shape at Module::Function(file:line): after trimming it must start with at , end with ), contain a last ( before a last :, have a purely-numeric line number, and a non-empty prefix before (. It returns the body (everything after at ), or None for malformed lines. - parse_more recognizes ... N more and returns N. Splitting into sections. Iterating trace.splitlines(), a Caused by: line closes the current section and opens a new one. Within a section, frames are collected in appearance order (most-recent → oldest), and the section's single omitted count is recorded. Non-frame, non-more lines (like noise/bad) are silently ignored. Reconstructing. For section i, if omitted > 0 and i > 0, it appends the last k = min(omitted, len(prev)) frames of full_sections[i-1] — the already-reconstructed prior stack, not its raw frames. This is why chained Caused by: sections inherit transitively (test 3). Clamping to len(prev) handles N larger than available frames. Building the forest. Each reconstructed stack is inserted from oldest to most-recent by iterating reversed(frames), creating child dicts as needed ({frame: child_subtree}). Shared call prefixes merge naturally, and disconnected roots coexist at the top level. Empty stacks are skipped, so an all-malformed section contributes nothing (test 5).

Space complexity: O(T), for storing the reconstructed stacks (each frame string referenced) plus the merged forest, whose total node count is bounded by the number of inserted frames.

Hints

  1. First reconstruct each section's full frame list; only then build the hierarchy.
  2. A trie built from oldest to most recent frames naturally merges shared call prefixes.

Part 3: Find the Deepest Frame of the Thrown Exception

# Find the Deepest Frame of the Thrown Exception Given a Java-style stack trace as a single string `trace`, return the **deepest frame of the actually-thrown exception**, or `None` if no such frame can be recovered. Implement: ```python def solution(trace): ... ``` ## Background A stack trace is divided into **sections**. The first section is the original error; each subsequent section is introduced by a line whose trimmed text begins with `Caused by:`. These sections form a *cause chain*, and the **last section in the chain is the actually-thrown exception** — the one whose deepest frame you must report. Within a section, frames are listed **most-recent first** (top) to **oldest last** (bottom). The call hierarchy is rooted at the oldest call, so the **deepest frame is the most-recent frame** — i.e. the **first** valid frame listed in the last section after expansion. ## Line types Parse the trace **line by line** (split on newlines). Each line, after trimming surrounding whitespace, is one of: - **Section header** — starts with `Caused by:`. Closes the current section and begins a new one. - **Frame line** — a valid frame has the trimmed form `at Module::Function(file:line)`. Specifically, after trimming it must: - start with `at ` (then take the remaining text as the **frame body**, e.g. `Module::Function(file:line)`), - end with `)`, - contain a `(` such that the text before the last `(` is **non-empty**, and - have, between the last `(` and the closing `)`, a `file:line` part whose portion **after the last `:` is a non-negative integer** (all digits). The value you ultimately return is this **frame body** (the text after `at `), e.g. `B::work(b.py:2)`. - **Abbreviation line** — the form `... N more` (trimmed text starts with `... ` and ends with ` more`, with `N` a non-negative integer in between). It means: this section's stack continues with the **last `N` frames of the previous section's fully reconstructed stack**. Each section contains **at most one** abbreviation line. - **Anything else** (including the leading exception-message line of a section, and any malformed frame) is **ignored**. ## Reconstruction Process sections in order. For each section, its reconstructed frame list is its own explicit valid frames followed by, **if it has a `... N more` and a previous section exists**, the last `k = min(N, len(previous_reconstructed))` frames of the **previous *fully reconstructed*** section (so inheritance composes across multiple nested causes). Inherited frames are appended **after** the section's own frames. ## Return value Return the **first frame** (index `0`, the most-recent / deepest) of the **last** section's reconstructed frame list. Return `None` when: - the trace is empty, or - the last section has **no** recoverable frame (no valid explicit frame and nothing recovered via `... N more`). Note: if the deepest section has no explicit valid frames, you may still recover its frames purely from `... N more`. ## Constraints - `0 <= len(trace) <= 200000` - The trace contains at most `10000` lines. - Each section contains at most one abbreviation line of the form `... N more`. - A valid frame line must start with `at ` after trimming and must end with `(file:line)` where `line` is a non-negative integer. ## Examples **Example 1** ``` Error at B::work(b.py:2) at A::main(a.py:1) ``` Single section; first valid frame is the deepest. Returns `B::work(b.py:2)`. **Example 2** ``` Outer at Service::run(s.py:3) at Main::main(m.py:1) Caused by: Inner at Parser::parse(p.py:2) ... 1 more ``` The last (thrown) section's first frame is `Parser::parse(p.py:2)`. Returns `Parser::parse(p.py:2)`. **Example 3** ``` Outer at X::x(x.py:2) at Main::main(m.py:1) Caused by: Inner bad line ... 2 more ``` The last section has no valid explicit frame; its frames are recovered from `... 2 more` (the last 2 frames of the reconstructed parent). The deepest is `X::x(x.py:2)`. Returns `X::x(x.py:2)`. **Example 4** ``` (empty string) ``` Returns `None`.

Constraints

  • 0 <= len(trace) <= 200000
  • The trace contains at most 10000 lines.
  • Each section contains at most one abbreviation line of the form '... N more'.
  • A valid frame line must start with 'at ' after trimming and must end with '(file:line)' where line is a non-negative integer.

Examples

Input: 'Error\nat B::work(b.py:2)\nat A::main(a.py:1)'

Expected Output: 'B::work(b.py:2)'

Explanation: With no nested cause, the thrown exception is the outermost section, and its deepest node is the most recent frame.

Input: 'Outer\nat Service::run(s.py:3)\nat Main::main(m.py:1)\nCaused by: Inner\nat Parser::parse(p.py:2)\n... 1 more'

Expected Output: 'Parser::parse(p.py:2)'

Explanation: The deepest cause expands to [Parser::parse, Main::main], so the deepest node is Parser::parse.

Approach

Approach. A stack trace is split into sections by Caused by: lines; the last section is the actually-thrown exception. We want its deepest frame, defined here as the most recent frame (index 0) of the last section after expanding any ... N more abbreviation. Parsing. Two helpers validate lines: - parse_frame accepts a line that, trimmed, starts with at and ends with (file:line) where line is all digits, and the part before ( is non-empty. It returns the canonical Module::Function(file:line) body; anything malformed returns None and is ignored. - parse_more reads ... N more, returning the integer N. We sweep every line once. A Caused by: flushes the current section and starts a fresh one. Otherwise we try to parse a frame (appended to current['frames']) or a ... N more (stored as current['omitted'], at most one per section). After the loop the final current is appended. Reconstruction. Sections are processed in order. For section i, if it has omitted > 0 and a previous section exists, we take the last k = min(omitted, len(parent)) frames of the previous fully reconstructed section (full_sections[i-1], not its raw frames) and append them. This is correct because ... N more means "the remaining frames are identical to the tail of the enclosing stack," and chaining off the already-expanded parent makes the rule compose across multiple nested causes. Result. The deepest path is the last reconstructed section; we return its element 0 (most recent frame), or None when the trace is empty or that section has no recoverable frame. Test 5 shows recovery purely from ... N more when explicit frames are malformed.

Time complexity: O(n + T), where n is the number of characters in the trace (one `splitlines` pass plus constant work per line) and T is the total number of frames across all reconstructed sections (each `... N more` can copy up to the parent's full length).

Space complexity: O(T) for the per-section frame lists and the reconstructed `full_sections` (which may duplicate inherited tail frames); bounded by the total reconstructed frame count.

Hints

  1. The thrown exception is the last section, not the first one.
  2. To expand '... N more', reuse the suffix of the previous section's reconstructed frame list.

Community answers

Answer by jayceeLOL

import re PATTERN = "at \w+\:\:\w+\(\w+\.*\w+\:\d+\)" def solution(trace): working_trace = trace.split('Caused by')[0] res = re.findall(PATTERN, working_trace) matches = [r[2:] for r in res] return matches simple regex search with group captures

Loading coding console...

Show the approach

Approach

Approach. We do a single linear scan over the lines of trace, stopping at the boundary of the outermost exception section, and keep only the lines that look like valid frames.

Section boundary. We split with trace.splitlines() and walk the lines top-to-bottom (already most-recent-to-oldest). The moment a line's strip()ped form startswith('Caused by:'), we break — everything from there on belongs to a nested cause and is ignored. An empty trace short-circuits to [] up front.

Frame validation (parse_frame). For each surviving line we trim whitespace and require:

  • it starts with 'at '; we then take body = stripped[3:].strip() (the frame without the at prefix);
  • body ends with ')';
  • using rfind('(') and rfind(':'), the last ( and last : exist with '(' before ':', and the : is not the final character (so a line number follows);
  • the substring after : up to the closing ) (body[colon+1:-1]) is all digits (isdigit()), i.e. a non-negative integer line number;
  • the part before ( (body[:open_paren], the Module::Function) is non-empty.

Lines failing any check return None and are skipped; valid ones append body (e.g. App::run(app.py:10)).

Why it's correct. Order is preserved because we append in iteration order and never reorder. Blank/malformed/unrelated lines simply fail validation. Using rfind for ( and : tolerates these characters appearing earlier in the module/function name while still anchoring on the trailing (file:line). The break guarantees only the first section is parsed.

Time complexity:
O(n), where n is the total number of characters in the trace. Each line is scanned a constant number of times (strip, rfind, slice), so the work is linear in the input size.
Space complexity:
O(n) in the worst case. splitlines() materializes the lines and the output list can hold up to O(n) characters of frame data; auxiliary work per line is O(1).