PracHub
QuestionsPremiumLearningGuidesInterview PrepNEWCoaches

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.

  • Medium
  • Anthropic
  • Coding & Algorithms
  • Software Engineer

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.

Quick Answer: 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

You are given a multi-line stack trace string. A valid frame line has the trimmed form 'at Module::Function(file:line)'. The lines already appear from most recent call to oldest call. Your task is to extract only the valid frames that belong to the outermost exception section. Rules: - Ignore blank, malformed, or unrelated lines. - Stop parsing as soon as you reach a line whose trimmed form starts with 'Caused by:'. - Return the frames in the same order they appear: 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 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.

Input: ''

Expected Output: []

Explanation: Empty input has no frames.

Input: 'TopError\nCaused by: Inner\nat X::f(x.py:1)'

Expected Output: []

Explanation: There are no outermost frames before the first cause section.

Input: 'Oops\n at One::only(one.py:1) '

Expected Output: ['One::only(one.py:1)']

Explanation: Leading and trailing spaces should be ignored when validating a frame line.

Solution

def solution(trace):
    def parse_frame(line):
        stripped = line.strip()
        if not stripped.startswith('at '):
            return None
        body = stripped[3:].strip()
        if not body.endswith(')'):
            return None
        open_paren = body.rfind('(')
        colon = body.rfind(':')
        if open_paren == -1 or colon == -1 or colon < open_paren or colon >= len(body) - 1:
            return None
        line_no = body[colon + 1:-1]
        if not line_no.isdigit() or not body[:open_paren]:
            return None
        return body

    if not trace:
        return []

    frames = []
    for line in trace.splitlines():
        if line.strip().startswith('Caused by:'):
            break
        frame = parse_frame(line)
        if frame is not None:
            frames.append(frame)
    return frames

Time complexity: O(n), where n is the number of characters in the trace.. Space complexity: O(f), where f is the number of extracted frames..

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

You are given a multi-line stack trace from one thread. The trace may contain nested exception sections introduced by 'Caused by:' and abbreviated tails written as '... N more'. A valid frame line has the trimmed form 'at Module::Function(file:line)'. Build the merged call hierarchy of all exception sections as a tree: - Each section starts at the beginning of the trace or immediately after a 'Caused by:' line. - Collect valid frame lines in that section from most recent to oldest. - If a section contains '... N more', append the last N frames from the previous section's fully reconstructed stack. If N is larger than the previous section length, append all available frames. - Ignore malformed lines. - Insert every fully reconstructed stack into a tree from oldest call to most recent call. Return the result as a forest represented by nested dictionaries: {frame: child_subtree}. If multiple disconnected roots exist, they all appear at the top level.

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.

Input: 'Top\nat A1::f(a1.py:4)\nat Root::main(root.py:1)\nCaused by: Mid\nat B1::g(b1.py:3)\n... 1 more\nCaused by: Low\nat C1::h(c1.py:2)\n... 2 more'

Expected Output: {'Root::main(root.py:1)': {'A1::f(a1.py:4)': {}, 'B1::g(b1.py:3)': {'C1::h(c1.py:2)': {}}}}

Explanation: Each deeper cause is expanded from the previous section, producing a shared prefix and a longer branch.

Input: ''

Expected Output: {}

Explanation: An empty trace has no hierarchy.

Input: 'E\nnoise\nat X::x(x.py:2)\nat Main::main(main.py:1)\nCaused by: Y\nbad\n... 5 more'

Expected Output: {'Main::main(main.py:1)': {'X::x(x.py:2)': {}}}

Explanation: The malformed line is ignored, and the oversized abbreviation copies only the available parent suffix.

Solution

def solution(trace):
    def parse_frame(line):
        stripped = line.strip()
        if not stripped.startswith('at '):
            return None
        body = stripped[3:].strip()
        if not body.endswith(')'):
            return None
        open_paren = body.rfind('(')
        colon = body.rfind(':')
        if open_paren == -1 or colon == -1 or colon < open_paren or colon >= len(body) - 1:
            return None
        line_no = body[colon + 1:-1]
        if not line_no.isdigit() or not body[:open_paren]:
            return None
        return body

    def parse_more(line):
        stripped = line.strip()
        if not stripped.startswith('... ') or not stripped.endswith(' more'):
            return None
        middle = stripped[4:-5].strip()
        if not middle.isdigit():
            return None
        return int(middle)

    if not trace:
        return {}

    sections = []
    current = {'frames': [], 'omitted': 0}

    for line in trace.splitlines():
        if line.strip().startswith('Caused by:'):
            sections.append(current)
            current = {'frames': [], 'omitted': 0}
            continue

        frame = parse_frame(line)
        if frame is not None:
            current['frames'].append(frame)
            continue

        omitted = parse_more(line)
        if omitted is not None:
            current['omitted'] = omitted

    sections.append(current)

    full_sections = []
    for i, section in enumerate(sections):
        frames = list(section['frames'])
        omitted = section['omitted']
        if omitted > 0 and i > 0:
            parent = full_sections[i - 1]
            k = min(omitted, len(parent))
            if k > 0:
                frames.extend(parent[-k:])
        full_sections.append(frames)

    forest = {}
    for frames in full_sections:
        if not frames:
            continue
        node = forest
        for frame in reversed(frames):
            if frame not in node:
                node[frame] = {}
            node = node[frame]

    return forest

Time complexity: O(n + T), where n is the number of characters and T is the total number of frames across all reconstructed sections.. Space complexity: O(T) for the reconstructed sections and merged tree..

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

A stack trace may contain nested exception sections introduced by 'Caused by:'. Treat the last section in the cause chain as the actually thrown exception. A valid frame line has the trimmed form 'at Module::Function(file:line)'. A section may also contain an abbreviation '... N more', which means its stack continues with the last N frames of the previous section's fully reconstructed stack. Your task is to return the deepest frame of the thrown exception's path in the call hierarchy. In this problem, the call hierarchy is rooted at the oldest call, so the deepest frame is the most recent frame of the last section after expansion. Rules: - Ignore malformed lines. - If the deepest section has no explicit valid frames, you may still recover frames from '... N more'. - If no valid frame can be recovered for the deepest section, return 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.

Input: 'Top\nat A::a(a.py:4)\nat Root::main(root.py:1)\nCaused by: Mid\nat B::b(b.py:3)\n... 1 more\nCaused by: Low\nat C::c(c.py:2)\n... 2 more'

Expected Output: 'C::c(c.py:2)'

Explanation: The final cause section reconstructs to [C::c, B::b, Root::main], and the deepest node is C::c.

Input: ''

Expected Output: None

Explanation: Empty input cannot produce a frame.

Input: 'Outer\nat X::x(x.py:2)\nat Main::main(m.py:1)\nCaused by: Inner\nbad line\n... 2 more'

Expected Output: 'X::x(x.py:2)'

Explanation: The deepest cause has no valid explicit frame, but expansion recovers [X::x, Main::main].

Solution

def solution(trace):
    def parse_frame(line):
        stripped = line.strip()
        if not stripped.startswith('at '):
            return None
        body = stripped[3:].strip()
        if not body.endswith(')'):
            return None
        open_paren = body.rfind('(')
        colon = body.rfind(':')
        if open_paren == -1 or colon == -1 or colon < open_paren or colon >= len(body) - 1:
            return None
        line_no = body[colon + 1:-1]
        if not line_no.isdigit() or not body[:open_paren]:
            return None
        return body

    def parse_more(line):
        stripped = line.strip()
        if not stripped.startswith('... ') or not stripped.endswith(' more'):
            return None
        middle = stripped[4:-5].strip()
        if not middle.isdigit():
            return None
        return int(middle)

    if not trace:
        return None

    sections = []
    current = {'frames': [], 'omitted': 0}

    for line in trace.splitlines():
        if line.strip().startswith('Caused by:'):
            sections.append(current)
            current = {'frames': [], 'omitted': 0}
            continue

        frame = parse_frame(line)
        if frame is not None:
            current['frames'].append(frame)
            continue

        omitted = parse_more(line)
        if omitted is not None:
            current['omitted'] = omitted

    sections.append(current)

    full_sections = []
    for i, section in enumerate(sections):
        frames = list(section['frames'])
        omitted = section['omitted']
        if omitted > 0 and i > 0:
            parent = full_sections[i - 1]
            k = min(omitted, len(parent))
            if k > 0:
                frames.extend(parent[-k:])
        full_sections.append(frames)

    deepest_section = full_sections[-1] if full_sections else []
    if not deepest_section:
        return None
    return deepest_section[0]

Time complexity: O(n + T), where n is the number of characters and T is the total number of frames across all reconstructed sections.. Space complexity: O(T)..

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.
Last updated: Apr 26, 2026

Loading coding console...

PracHub

Master your tech interviews with 7,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Implement a Time-Aware Task Manager - Anthropic (hard)
  • Implement a Simplified DNS Resolver - Anthropic (hard)
  • Implement a Least-Recently-Used Cache - Anthropic (medium)
  • Convert Samples into Event Intervals - Anthropic (medium)
  • Convert State Stream to Events - Anthropic (medium)