diff --git a/src/components/DebugPanel.vue b/src/components/DebugPanel.vue index c921f7c..2be1a9f 100644 --- a/src/components/DebugPanel.vue +++ b/src/components/DebugPanel.vue @@ -101,16 +101,21 @@ const nextLine = computed(() => { return undefined }) -// 调试信息相关 +// 当前步骤对象 +const currentTraceEntry = computed(() => { + return debugData.value?.trace?.[currentStep.value] ?? null +}) + +// 调试信息相关:优先显示栈顶(高亮)帧的局部变量,没有则用全局 const currentVariables = computed(() => { - if ( - debugData.value && - debugData.value.trace && - debugData.value.trace[currentStep.value] - ) { - return debugData.value.trace[currentStep.value].globals || {} + const entry = currentTraceEntry.value + if (!entry) return {} + const stack = entry.stack_to_render ?? [] + const topFrame = stack.find((f: any) => f.is_highlighted) ?? stack[stack.length - 1] + if (topFrame && topFrame.encoded_locals) { + return { ...(entry.globals ?? {}), ...topFrame.encoded_locals } } - return {} + return entry.globals ?? {} }) // 格式化变量显示 @@ -120,31 +125,43 @@ const formattedVariables = computed(() => { return [] } - return Object.entries(variables).map(([key, value]) => { - // 处理特殊类型 - let displayValue = "" - let displayType = typeof value + const heap: Record = + debugData.value?.trace?.[currentStep.value]?.heap ?? {} - if ( - Array.isArray(value) && - value.length === 2 && - value[0] === "IMPORTED_FAUX_PRIMITIVE" && - value[1] === "imported object" - ) { - displayValue = "" - displayType = "function" - } else if (typeof value === "object" && value !== null) { - displayValue = JSON.stringify(value, null, 2) - } else { - displayValue = String(value) - } + return Object.entries(variables) + .filter(([, value]) => { + // 隐藏导入的模块/函数占位符 + if ( + Array.isArray(value) && + value[0] === "IMPORTED_FAUX_PRIMITIVE" + ) + return false + if (Array.isArray(value) && value[0] === "FUNCTION") return false + return true + }) + .map(([key, value]) => { + const displayValue = decodeValue(value, heap) + // resolve REF before checking tag + const resolved = + Array.isArray(value) && value[0] === "REF" + ? heap[String(value[1])] + : value + const tag = Array.isArray(resolved) ? resolved[0] : null + const typeMap: Record = { + LIST: "list", + TUPLE: "tuple", + SET: "set", + DICT: "dict", + FUNCTION: "function", + INSTANCE: "object", + INSTANCE_PPRINT: "object", + CLASS: "class", + } + const displayType = + tag && typeMap[tag] ? typeMap[tag] : typeof value - return { - name: key, - value: displayValue, - type: displayType, - } - }) + return { name: key, value: displayValue, type: displayType } + }) }) // 计算输出行数 @@ -162,7 +179,15 @@ const currentLineText = computed(() => { ) { const step = debugData.value.trace[currentStep.value] const isLastStep = currentStep.value === debugData.value.trace.length - 1 - const eventText = isLastStep ? "" : getEventText(step.event) + // 异常/输入等待/超步数:保留事件提示,让用户能看到状态 + const isStatusEvent = + step.event === "exception" || + step.event === "uncaught_exception" || + step.event === "raw_input" || + step.event === "mouse_input" || + step.event === "instruction_limit_reached" + const eventText = + isLastStep && !isStatusEvent ? "" : getEventText(step.event) const stepText = isLastStep ? "最后一步" : `当前第${currentStep.value + 1}步` @@ -188,63 +213,118 @@ const nextLineText = computed(() => { }) // ==================== 工具函数 ==================== + +/** + * 将 pg_encoder 编码值解析为可读字符串,heap 用于解引用 REF + */ +function decodeValue(val: any, heap: Record, depth = 0): string { + if (depth > 8) return "..." + if (val === null || val === undefined) return "None" + if (typeof val === "boolean") return val ? "True" : "False" + if (typeof val === "string") return JSON.stringify(val) + if (!Array.isArray(val)) return String(val) + + const [tag, ...rest] = val + switch (tag) { + case "REF": { + const obj = heap[String(rest[0])] + return obj ? decodeValue(obj, heap, depth + 1) : `REF(${rest[0]})` + } + case "LIST": + return "[" + rest.map((e: any) => decodeValue(e, heap, depth + 1)).join(", ") + "]" + case "TUPLE": + return rest.length === 1 + ? "(" + decodeValue(rest[0], heap, depth + 1) + ",)" + : "(" + rest.map((e: any) => decodeValue(e, heap, depth + 1)).join(", ") + ")" + case "SET": + return "{" + rest.map((e: any) => decodeValue(e, heap, depth + 1)).join(", ") + "}" + case "DICT": + return ( + "{" + + rest + .map(([k, v]: [any, any]) => decodeValue(k, heap, depth + 1) + ": " + decodeValue(v, heap, depth + 1)) + .join(", ") + + "}" + ) + case "FUNCTION": + return `` + case "INSTANCE": + return `<${rest[0]} instance>` + case "INSTANCE_PPRINT": + // [class_name, __str__ value, [attr, value], ...] + return `<${rest[0]}: ${rest[1]}>` + case "CLASS": + return `` + case "SPECIAL_FLOAT": + return String(rest[0]) + case "IMPORTED_FAUX_PRIMITIVE": + return String(rest[0]) + default: + return rest.length === 1 ? String(rest[0]) : JSON.stringify(val) + } +} + /** * 获取事件类型的中文描述 */ function getEventText(event: string): string { switch (event) { case "step_line": - return "" // 普通执行不显示额外文字 + return "" case "call": return "(调用函数)" case "return": return "(函数返回)" case "exception": - return "(异常)" case "uncaught_exception": return "(异常)" case "raw_input": + case "mouse_input": return "(等待输入)" + case "instruction_limit_reached": + return "(超出步数上限)" default: return event || "" } } -// 输出相关 +// 输出:stdout 和异常信息合并显示,不互相覆盖 const currentOutput = computed(() => { + const entry = currentTraceEntry.value + if (!entry) return output.value || "" + + let outputText = (entry.stdout ?? "").trimEnd() + if ( - debugData.value && - debugData.value.trace && - debugData.value.trace.length > 0 + entry.event === "exception" || + entry.event === "uncaught_exception" || + entry.event === "instruction_limit_reached" ) { - let outputText = "" - - for (let i = 0; i <= currentStep.value; i++) { - const step = debugData.value.trace[i] - if (step) { - if (step.event === "exception" || step.event === "uncaught_exception") { - if (step.exception_msg) { - outputText = step.exception_msg - } - } else if (step.stdout) { - outputText = step.stdout - } - } + if (entry.exception_msg) { + outputText = outputText + ? outputText + "\n" + entry.exception_msg + : entry.exception_msg } + } - outputText = outputText.trimEnd() + return outputText +}) - const hasException = debugData.value.trace.some( - (step: any) => - step.event === "exception" || step.event === "uncaught_exception", +// 把外部状态的同步放到 watch 里(computed 不能有副作用) +watch( + [currentOutput, () => debugData.value?.trace], + ([text, trace]) => { + if (!trace) return + output.value = text + const hasException = trace.some( + (s: any) => + s.event === "exception" || + s.event === "uncaught_exception" || + s.event === "instruction_limit_reached", ) status.value = hasException ? Status.RuntimeError : Status.Accepted - - output.value = outputText - return outputText - } - return output.value || "" -}) + }, +) // ==================== 主要功能函数 ==================== /** @@ -330,14 +410,21 @@ function autoRun() { if (!debugData.value || !debugData.value.trace) return if (isAutoRunActive.value) { - // 停止自动运行 pauseAutoRun() isAutoRunning.value = false - } else { - // 开始自动运行 - isAutoRunning.value = true - resumeAutoRun() + return } + + const trace = debugData.value.trace + // 已经到末尾或停在等待输入,没法继续自动运行 + if (currentStep.value >= trace.length - 1) return + if (trace[currentStep.value]?.event === "raw_input") { + message.info("当前停在等待输入步,请先重新运行并提供输入") + return + } + + isAutoRunning.value = true + resumeAutoRun() } diff --git a/src/desktop/CodeSection.vue b/src/desktop/CodeSection.vue index 5a5a972..6038042 100644 --- a/src/desktop/CodeSection.vue +++ b/src/desktop/CodeSection.vue @@ -23,41 +23,33 @@ function copy() { } /** - * 检查调试数据是否需要输入但用户没有提供足够的输入 + * trace 末尾停在 raw_input 即说明输入不足 + * (pg_logger 在缺输入时会立刻 done=True,trace 中至多只有 1 个 raw_input 事件, + * 所以不能用计数对比,只能看末尾) */ -function needsInputButNotProvided( - debugData: any, - providedInputs: string[], -): boolean { - if (!debugData?.trace || debugData.trace.length === 0) { - return false - } - - const lastStep = debugData.trace[debugData.trace.length - 1] - // 如果最后一步是 raw_input,说明程序在等待输入,用户提供的输入不足 - if (lastStep.event === "raw_input") { - // 统计 trace 中所有的 raw_input 事件数量(程序需要的输入数量) - const requiredInputCount = debugData.trace.filter( - (step: any) => step.event === "raw_input", - ).length - - // 如果用户提供的输入数量不足,返回 true - return providedInputs.length < requiredInputCount - } - - return false +function endsAtRawInput(debugData: any): boolean { + const trace = debugData?.trace + if (!trace?.length) return false + return trace[trace.length - 1].event === "raw_input" } async function handleDebug() { const inputs = input.value ? input.value.split("\n").filter((line) => line.trim() !== "") : [] - const res = await debug(code.value, inputs) + + let res + try { + res = await debug(code.value, inputs) + } catch (err: any) { + message.error(`调试请求失败: ${err?.message ?? err}`) + return + } + debugData.value = res.data - // 检查是否需要输入但用户没有提供足够的输入 - if (needsInputButNotProvided(res.data, inputs)) { - message.warning("程序需要输入,请在输入框输入内容后重新点击调试按钮") + if (endsAtRawInput(res.data)) { + message.warning("程序需要更多输入,请在输入框补全后重新点击调试") return }