Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,13 @@ let package = Package(
),
.testTarget(
name: "LitheDebugModuleTests",
dependencies: ["LitheDebugModule", "LitheApplicationKernel", .product(name: "Testing", package: "swift-testing")],
dependencies: ["LitheDebugModule", "LitheApplicationKernel", "LitheRustCore", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheDebugModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "LitheExecutionModuleTests",
dependencies: ["LitheExecutionModule", "LitheApplicationKernel", "LitheCoreContracts", .product(name: "Testing", package: "swift-testing")],
dependencies: ["LitheExecutionModule", "LitheApplicationKernel", "LitheCoreContracts", "LitheRustCore", .product(name: "Testing", package: "swift-testing")],
path: "macos/Tests/LitheExecutionModuleTests",
swiftSettings: [.swiftLanguageMode(.v6)]
),
Expand Down
4 changes: 4 additions & 0 deletions macos/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2718,6 +2718,8 @@
"No tasks yet" = "No tasks yet";
"No test runner is configured for %@." = "No test runner is configured for %@.";
"No tests discovered." = "No tests discovered.";
"No tests matched the selection." = "No tests matched the selection.";
"No tests were found." = "No tests were found.";
"No textual diff available" = "No textual diff available";
"No textual difference" = "No textual difference";
"No tools running" = "No tools running";
Expand Down Expand Up @@ -10340,6 +10342,8 @@
"system Maven" = "system Maven";
"test Java" = "test Java";
"test resources" = "test resources";
"tests.outcome.noTestsFound" = "No tests were found.";
"tests.outcome.noTestsMatched" = "No tests matched the selection.";
"the project root" = "the project root";
"ui.browse" = "Browse";
"ui.cancel" = "Cancel";
Expand Down
4 changes: 4 additions & 0 deletions macos/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2718,6 +2718,8 @@
"No tasks yet" = "暂无任务";
"No test runner is configured for %@." = "没有为 %@ 配置测试运行器。";
"No tests discovered." = "未发现测试。";
"No tests matched the selection." = "没有测试匹配所选范围。";
"No tests were found." = "没有找到任何测试。";
"No textual diff available" = "没有可用的文本 Diff";
"No textual difference" = "没有文本差异";
"No tools running" = "暂无工具运行";
Expand Down Expand Up @@ -10340,6 +10342,8 @@
"system Maven" = "系统 Maven";
"test Java" = "测试代码";
"test resources" = "测试资源";
"tests.outcome.noTestsFound" = "没有找到任何测试。";
"tests.outcome.noTestsMatched" = "没有测试匹配所选范围。";
"the project root" = "项目根目录";
"ui.browse" = "浏览";
"ui.cancel" = "取消";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ final class DebugFeatureGraph: NSObject, DebugServiceGraph {

init(
adapterSessions: DebugAdapterSessionManager,
paging: any DebugPagingPlanning,
logSink: (any LogSink)? = nil,
breakpointPersistence: (any DebugBreakpointPersisting)? = nil,
breakpointRelocator: (any DebugBreakpointRelocating)? = nil,
Expand All @@ -21,6 +22,7 @@ final class DebugFeatureGraph: NSObject, DebugServiceGraph {
) {
self.adapterSessions = adapterSessions
genericFeature = GenericDebugFeatureModel(
paging: paging,
logSink: logSink,
sessions: adapterSessions,
breakpointPersistence: breakpointPersistence,
Expand Down
19 changes: 19 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustRunDebugReducers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import LitheCoreContracts

/// Pure Core reducers for test verdicts and debugger paging; no native I/O.
extension RustCoreBridge: DebugPagingCoreCommandExecuting, TestRunOutcomeCoreCommandExecuting {
func executeDebugPagingCommand<Payload: Encodable, Response: Decodable>(
_ command: String,
payload: Payload
) throws -> Response {
try executeResult(command: command, payload: payload).get()
}

func executeTestRunOutcomeCommand<Payload: Encodable, Response: Decodable>(
_ command: String,
payload: Payload
) throws -> Response {
try executeResult(command: command, payload: payload).get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ final class MacServiceContainer {
let graph = ExecutionFeatureGraph(
run: runService,
tests: LanguageTestService(
outcomeResolver: rustCore,
catalog: languagePackRegistry.catalog,
registry: languagePackRegistry.testProviders,
executableResolver: executableResolver,
Expand Down Expand Up @@ -924,6 +925,7 @@ final class MacServiceContainer {
debugAdapterSessionsBox.manager = adapterSessions
let graph = DebugFeatureGraph(
adapterSessions: adapterSessions,
paging: rustCore,
logSink: logSink,
breakpointPersistence: debugBreakpointStore,
breakpointRelocator: rustCore,
Expand Down
258 changes: 258 additions & 0 deletions macos/Sources/LitheCoreContracts/Debug/DebugPagingContracts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import Foundation

/// Core-owned paging cursor of one variable container (`debug.planVariablePage`).
///
/// The host keeps the cursor for each loaded container and echoes it back with
/// every page; only `next` is meant to be read. The other fields round-trip
/// unchanged so Core, not the host, decides when a container is exhausted.
public struct DebugVariablePageCursor: Codable, Equatable, Sendable {
/// The `variables` request to send next.
public struct Request: Codable, Equatable, Sendable {
public let filter: DebugVariableFilter?
public let start: Int
public let count: Int
/// Children still to load across every remaining kind, when the adapter reported counts.
public let remainingCount: Int?
}

public struct Segment: Codable, Equatable, Sendable {
public let filter: DebugVariableFilter?
public let nextStart: Int
public let totalCount: Int?
}

public let variablesReference: Int
public let pageSize: Int
public let segments: [Segment]
public let loadedPages: [String]
/// Absent once the container is exhausted.
public let next: Request?
}

/// Identity fields of one returned child; Core fingerprints them to detect repeated pages.
public struct DebugVariablePageItem: Codable, Equatable, Sendable {
public let name: String
public let value: String
public let type: String?
public let evaluateName: String?
public let variablesReference: Int

public init(_ variable: DebugVariable) {
name = variable.name
value = variable.value
type = variable.type
evaluateName = variable.evaluateName
variablesReference = variable.variablesReference
}
}

/// Result of merging one page: whether to show it, and the cursor that follows.
public struct DebugVariablePageReduction: Codable, Equatable, Sendable {
public let accept: Bool
public let state: DebugVariablePageCursor
}

/// One direction a disassembly fetch extends the listing.
public enum DebugDisassemblyDirection: String, Codable, Equatable, Sendable {
case around, before, after
}

/// Core-owned cursor of one bounded disassembly request sequence
/// (`debug.planDisassemblyWindow`). Hosts send `request` and echo the fetch with the reply.
public struct DebugDisassemblyFetch: Codable, Equatable, Sendable {
public struct Request: Codable, Equatable, Sendable {
public let memoryReference: String
public let instructionOffset: Int
public let instructionCount: Int
}

public let direction: DebugDisassemblyDirection
public let reference: String
public let phase: String
public let count: Int
public let instructionsBefore: Int
public let windowFailure: String?
public let request: Request
}

/// What the adapter answered to a fetch's request.
public enum DebugDisassemblyReply: Equatable, Sendable {
case instructions([DebugDisassembledInstruction])
/// The adapter answered `disassemble` with `success: false`.
case rejected(String)
/// Transport failure, timeout or cancellation.
case failed(String)
}

/// The next step of a disassembly fetch.
public enum DebugDisassemblyStep: Equatable, Sendable {
case request(DebugDisassemblyFetch)
/// `keep` indexes the reply's instructions to list, in order.
case page(keep: [Int], exhausted: Bool)
case failed(String)
}

/// Shared paging rules for the variables tree and the disassembly listing.
///
/// Core owns when to ask for more, how to recover from refused windows and when
/// a direction or container is exhausted; hosts keep page sizes, rows,
/// expansion, viewport and scroll anchors.
public protocol DebugPagingPlanning: Sendable {
func planVariablePage(
variablesReference: Int,
namedVariables: Int,
indexedVariables: Int,
pageSize: Int
) throws -> DebugVariablePageCursor
func reduceVariablePage(
_ state: DebugVariablePageCursor,
requested: DebugVariablePageCursor.Request,
page: [DebugVariablePageItem],
newItemCount: Int
) throws -> DebugVariablePageReduction
func planDisassemblyWindow(
direction: DebugDisassemblyDirection,
reference: String,
count: Int,
instructionsBefore: Int
) throws -> DebugDisassemblyFetch
func reduceDisassemblyPage(
_ fetch: DebugDisassemblyFetch,
reply: DebugDisassemblyReply,
knownAddresses: [String]
) throws -> DebugDisassemblyStep
}

/// A JSON command transport to Rust Core, shared by the app bridge and tests
/// that link the same Core library.
public protocol DebugPagingCoreCommandExecuting: DebugPagingPlanning {
func executeDebugPagingCommand<Payload: Encodable, Response: Decodable>(
_ command: String,
payload: Payload
) throws -> Response
}

public extension DebugPagingCoreCommandExecuting {
func planVariablePage(
variablesReference: Int,
namedVariables: Int,
indexedVariables: Int,
pageSize: Int
) throws -> DebugVariablePageCursor {
return try executeDebugPagingCommand("debug.planVariablePage", payload: PlanVariablePagePayload(
variablesReference: variablesReference,
namedVariables: namedVariables,
indexedVariables: indexedVariables,
pageSize: pageSize
))
}

func reduceVariablePage(
_ state: DebugVariablePageCursor,
requested: DebugVariablePageCursor.Request,
page: [DebugVariablePageItem],
newItemCount: Int
) throws -> DebugVariablePageReduction {
return try executeDebugPagingCommand("debug.reduceVariablePage", payload: ReduceVariablePagePayload(
state: state,
requested: requested,
page: page,
newItemCount: newItemCount
))
}

func planDisassemblyWindow(
direction: DebugDisassemblyDirection,
reference: String,
count: Int,
instructionsBefore: Int
) throws -> DebugDisassemblyFetch {
return try executeDebugPagingCommand("debug.planDisassemblyWindow", payload: PlanDisassemblyWindowPayload(
direction: direction,
reference: reference,
count: count,
instructionsBefore: instructionsBefore
))
}

func reduceDisassemblyPage(
_ fetch: DebugDisassemblyFetch,
reply: DebugDisassemblyReply,
knownAddresses: [String]
) throws -> DebugDisassemblyStep {
let encodedReply: ReduceDisassemblyPageReply
switch reply {
case .instructions(let instructions):
encodedReply = ReduceDisassemblyPageReply(kind: "instructions", instructions: instructions.map {
ReduceDisassemblyPageInstruction(address: $0.address, instruction: $0.instruction, instructionBytes: $0.instructionBytes)
}, message: nil)
case .rejected(let message):
encodedReply = ReduceDisassemblyPageReply(kind: "rejected", instructions: nil, message: message)
case .failed(let message):
encodedReply = ReduceDisassemblyPageReply(kind: "failed", instructions: nil, message: message)
}
let step: ReduceDisassemblyPageStep = try executeDebugPagingCommand("debug.reduceDisassemblyPage", payload: ReduceDisassemblyPagePayload(
fetch: fetch,
reply: encodedReply,
knownAddresses: knownAddresses
))
switch (step.kind, step.fetch, step.keep, step.exhausted, step.message) {
case ("request", let fetch?, _, _, _): return .request(fetch)
case ("page", _, let keep?, let exhausted?, _): return .page(keep: keep, exhausted: exhausted)
case ("failed", _, _, _, let message?): return .failed(message)
default: throw DebugPagingCoreError.invalidStep(step.kind)
}
}
}

/// A Core reply that does not match the paging contract.
public enum DebugPagingCoreError: Error, Equatable, Sendable {
case invalidStep(String)
}

private struct PlanVariablePagePayload: Encodable {
let variablesReference: Int
let namedVariables: Int
let indexedVariables: Int
let pageSize: Int
}

private struct ReduceVariablePagePayload: Encodable {
let state: DebugVariablePageCursor
let requested: DebugVariablePageCursor.Request
let page: [DebugVariablePageItem]
let newItemCount: Int
}

private struct PlanDisassemblyWindowPayload: Encodable {
let direction: DebugDisassemblyDirection
let reference: String
let count: Int
let instructionsBefore: Int
}

private struct ReduceDisassemblyPageInstruction: Encodable {
let address: String
let instruction: String
let instructionBytes: String?
}

private struct ReduceDisassemblyPageReply: Encodable {
let kind: String
let instructions: [ReduceDisassemblyPageInstruction]?
let message: String?
}

private struct ReduceDisassemblyPagePayload: Encodable {
let fetch: DebugDisassemblyFetch
let reply: ReduceDisassemblyPageReply
let knownAddresses: [String]
}

private struct ReduceDisassemblyPageStep: Decodable {
let kind: String
let fetch: DebugDisassemblyFetch?
let keep: [Int]?
let exhausted: Bool?
let message: String?
}
Loading
Loading