Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ public struct VisuStructSimulationState: Equatable, Sendable {
public var variables: [String: VisuStructSimulationValue]
public var outputLines: [String]
public var inputRequest: VisuStructSimulationInputRequest?
public var inputError: String?
/// Fehler zur letzten Eingabe (z. B. keine Zahl); Text in der App über `VisuStructSimulationNotice` lokalisieren.
public var inputErrorNotice: VisuStructSimulationNotice?
public var lastTrace: String?
public var message: String?
/// Hinweis nach dem letzten Schritt (Ende, Fehler); `nil` während die Simulation normal läuft.
public var notice: VisuStructSimulationNotice?
/// Nach einem Fehler bleibt die Simulation auf dem fehlerhaften Schritt stehen, bis sie zurückgesetzt wird.
public var isHalted: Bool

Expand All @@ -113,22 +115,29 @@ public struct VisuStructSimulationState: Equatable, Sendable {
variables: [String: VisuStructSimulationValue] = [:],
outputLines: [String] = [],
inputRequest: VisuStructSimulationInputRequest? = nil,
inputError: String? = nil,
inputErrorNotice: VisuStructSimulationNotice? = nil,
lastTrace: String? = nil,
message: String? = nil,
notice: VisuStructSimulationNotice? = nil,
isHalted: Bool = false
) {
self.stepIndex = stepIndex
self.variables = variables
self.outputLines = outputLines
self.inputRequest = inputRequest
self.inputError = inputError
self.inputErrorNotice = inputErrorNotice
self.lastTrace = lastTrace
self.message = message
self.notice = notice
self.isHalted = isHalted
}
}

extension VisuStructSimulationState {
/// Englischer Text von `notice` (Tests, Protokolle).
public var message: String? { notice?.englishText }
/// Englischer Text von `inputErrorNotice`.
public var inputError: String? { inputErrorNotice?.englishText }
}

public struct VisuStructSimulationEngine: Equatable, Sendable {
public private(set) var steps: [VisuStructSimulationStep]
public private(set) var state: VisuStructSimulationState
Expand Down Expand Up @@ -157,7 +166,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
public mutating func step() {
guard canStep else {
if state.inputRequest == nil && !state.isHalted {
state.message = "Simulation finished."
state.notice = .finished
}
return
}
Expand Down Expand Up @@ -196,13 +205,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
return
}
if state.inputRequest != nil {
state.message = nil
state.notice = nil
return
}
state.stepIndex += 1
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
state.notice = state.stepIndex >= steps.count ? .finished : nil
} catch {
state.message = error.localizedDescription
state.notice = VisuStructSimulationNotice(error)
state.isHalted = true
}
}
Expand All @@ -214,13 +223,13 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
try assignValue(value, to: request.variableName)
state.lastTrace = "\(request.variableName) = input -> \(value.description)"
} catch {
state.inputError = error.localizedDescription
state.inputErrorNotice = VisuStructSimulationNotice(error)
return
}
state.inputRequest = nil
state.inputError = nil
state.inputErrorNotice = nil
state.stepIndex += 1
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
state.notice = state.stepIndex >= steps.count ? .finished : nil
}

private static func parseInputValue(_ rawValue: String, as valueType: VisuStructSimulationValueType) throws -> VisuStructSimulationValue {
Expand All @@ -230,12 +239,12 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
switch valueType {
case .int:
guard let value = Int(trimmed) else {
throw SimulationError.invalidInput("Please enter a whole number.")
throw SimulationError.invalidInput(.wholeNumberExpected)
}
return .int(value)
case .double:
guard let value = Double(trimmed) else {
throw SimulationError.invalidInput("Please enter a number.")
throw SimulationError.invalidInput(.numberExpected)
}
return .double(value)
case .string:
Expand Down Expand Up @@ -346,7 +355,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
steps.remove(at: state.stepIndex)
steps.insert(contentsOf: branchSteps, at: state.stepIndex)
state.lastTrace = "\(condition) -> \(result ? "true" : "false")"
state.message = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
state.notice = !branchSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
}

private mutating func executeSwitchSelection(_ step: VisuStructSimulationStep) throws {
Expand All @@ -366,7 +375,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
steps.remove(at: state.stepIndex)
steps.insert(contentsOf: caseSteps, at: state.stepIndex)
state.lastTrace = "\(selector) -> \(selectedCase.name)"
state.message = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
state.notice = !caseSteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
}

private mutating func executeForLoopStart(_ step: VisuStructSimulationStep) throws {
Expand Down Expand Up @@ -419,7 +428,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
steps.insert(contentsOf: bodySteps + [incrementStep, conditionStep], at: state.stepIndex)
} else {
steps.remove(at: state.stepIndex)
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
state.notice = state.stepIndex >= steps.count ? .finished : nil
}
}

Expand Down Expand Up @@ -449,10 +458,10 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
)
steps.remove(at: state.stepIndex)
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
} else {
steps.remove(at: state.stepIndex)
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
state.notice = state.stepIndex >= steps.count ? .finished : nil
}
}

Expand All @@ -469,7 +478,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
)
steps.remove(at: state.stepIndex)
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
state.message = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? "Simulation finished." : nil)
state.notice = !bodySteps.isEmpty ? nil : (state.stepIndex >= steps.count ? .finished : nil)
}

private mutating func executeDoUntilCondition(_ step: VisuStructSimulationStep) throws {
Expand All @@ -491,7 +500,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
steps.insert(contentsOf: bodySteps + [conditionStep], at: state.stepIndex)
} else {
steps.remove(at: state.stepIndex)
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
state.notice = state.stepIndex >= steps.count ? .finished : nil
}
}

Expand All @@ -509,14 +518,14 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
steps.remove(at: state.stepIndex)
steps.insert(contentsOf: bodySteps + [loopStep], at: state.stepIndex)
state.lastTrace = "true -> true"
state.message = !bodySteps.isEmpty ? nil : "Infinite loop has no body."
state.notice = !bodySteps.isEmpty ? nil : .infiniteLoopWithoutBody
}

private mutating func execute(statement rawText: String) throws {
let text = rawText.trimmingCharacters(in: .whitespacesAndNewlines)
if text.lowercased().hasPrefix("input:") {
state.inputRequest = try parseInputRequest(text)
state.inputError = nil
state.inputErrorNotice = nil
return
}

Expand Down Expand Up @@ -1343,20 +1352,53 @@ private enum SimulationError: LocalizedError, Equatable {
case invalidExpression(String)
case unknownVariable(String)
case divisionByZero
case invalidInput(String)
case invalidInput(VisuStructSimulationNotice)

var notice: VisuStructSimulationNotice {
switch self {
case .unsupportedStatement(let text): return .unsupportedStatement(text)
case .invalidExpression(let text): return .invalidExpression(text)
case .unknownVariable(let name): return .unknownVariable(name)
case .divisionByZero: return .divisionByZero
case .invalidInput(let notice): return notice
}
}

var errorDescription: String? { notice.englishText }
}

/// Meldungen der Simulation, unabhängig von der Sprache; die App übersetzt sie.
public enum VisuStructSimulationNotice: Equatable, Sendable {
case finished
case infiniteLoopWithoutBody
case unsupportedStatement(String)
case invalidExpression(String)
case unknownVariable(String)
case divisionByZero
case wholeNumberExpected
case numberExpected
/// Fehler aus einer anderen Quelle, nur als Text bekannt.
case other(String)

init(_ error: Error) {
if let simulationError = error as? SimulationError {
self = simulationError.notice
} else {
self = .other(error.localizedDescription)
}
}

var errorDescription: String? {
public var englishText: String {
switch self {
case .unsupportedStatement(let text):
return "Unsupported statement: \(text)"
case .invalidExpression(let text):
return "Invalid expression: \(text)"
case .unknownVariable(let name):
return "Unknown variable: \(name)"
case .divisionByZero:
return "Division by zero."
case .invalidInput(let message):
return message
case .finished: return "Simulation finished."
case .infiniteLoopWithoutBody: return "Infinite loop has no body."
case .unsupportedStatement(let text): return "Unsupported statement: \(text)"
case .invalidExpression(let text): return "Invalid expression: \(text)"
case .unknownVariable(let name): return "Unknown variable: \(name)"
case .divisionByZero: return "Division by zero."
case .wholeNumberExpected: return "Please enter a whole number."
case .numberExpected: return "Please enter a number."
case .other(let text): return text
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class SimulationEngineTests: XCTestCase {
XCTAssertEqual(engine.state.inputRequest?.variableName, "n")

engine.provideInput("abc")
XCTAssertNotNil(engine.state.inputError)
XCTAssertEqual(engine.state.inputErrorNotice, .wholeNumberExpected)
XCTAssertNotNil(engine.state.inputRequest)

engine.provideInput("21")
Expand Down Expand Up @@ -107,7 +107,18 @@ final class SimulationEngineTests: XCTestCase {
])
var engine = VisuStructSimulationEngine(document: document)
engine.step()
XCTAssertEqual(engine.state.notice, .divisionByZero)
XCTAssertEqual(engine.state.message, "Division by zero.")
XCTAssertNil(engine.state.variables["x"])
}

func testFinishedNoticeAfterLastStep() {
let document = VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int x = 1"]),
])
var engine = VisuStructSimulationEngine(document: document)
engine.step()
XCTAssertEqual(engine.state.notice, .finished)
XCTAssertEqual(engine.state.message, "Simulation finished.")
}
}
27 changes: 24 additions & 3 deletions VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/AppLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,37 @@ enum AppLanguage: String, CaseIterable, Identifiable {
return b
}

/// Übersetzung zu `key`; fehlt sie in dieser Sprache, der englische Text (nie der Schlüssel selbst).
func tr(_ key: String) -> String {
let text: String
if self == .javaLike {
return NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
text = NSLocalizedString(key, tableName: Self.javaStringsTable, bundle: Bundle.main, value: key, comment: "")
} else {
text = NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
}
return NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "")
if text == key, self != .english {
return AppLanguage.english.tr(key)
}
return text
}

/// Sprache des Geräts (bzw. die für VisuStruct in den Systemeinstellungen gewählte), sofern die App sie anbietet; sonst Englisch.
static var deviceDefault: AppLanguage {
for code in Bundle.main.preferredLocalizations {
if let lang = AppLanguage(rawValue: code), lang != .javaLike { return lang }
}
return .english
}

/// Erster Start ohne gespeicherte Sprache: Gerätesprache übernehmen, damit auch die Systemeinstellungen sie anzeigen.
static func storeDeviceDefaultIfNeeded() {
guard UserDefaults.standard.string(forKey: storageKey) == nil else { return }
UserDefaults.standard.set(deviceDefault.rawValue, forKey: storageKey)
}

/// Für `ContentView.init`, bevor `Environment` steht.
static func currentFromDefaults() -> AppLanguage {
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.english.rawValue
let code = UserDefaults.standard.string(forKey: storageKey) ?? AppLanguage.deviceDefault.rawValue
return AppLanguage(rawValue: code) ?? .english
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ struct SimulationStatePanel: View {
prompt: engine.state.inputRequest?.prompt,
variableName: engine.state.inputRequest?.variableName,
valueType: engine.state.inputRequest?.valueType,
inputError: engine.state.inputError,
inputError: engine.state.inputErrorNotice?.localized(in: lang),
inputText: $inputText,
onSubmit: onSubmitInput
)
Expand Down Expand Up @@ -160,7 +160,7 @@ struct SimulationStatePanel: View {
}
.layoutPriority(flexibleCardsFillHeight ? 1 : 0)

if let message = engine.state.message {
if let message = engine.state.notice?.localized(in: lang) {
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
Expand Down Expand Up @@ -301,3 +301,31 @@ struct SimulationPlaceholderCard<Content: View>: View {
}
}


// MARK: - Meldungen der Simulation

extension VisuStructSimulationNotice {
/// Meldung der Simulation in der App-Sprache.
func localized(in lang: AppLanguage) -> String {
switch self {
case .finished:
return lang.tr("sim_notice_finished")
case .infiniteLoopWithoutBody:
return lang.tr("sim_notice_infinite_loop_empty")
case .unsupportedStatement(let text):
return String(format: lang.tr("sim_notice_unsupported_statement_fmt"), text)
case .invalidExpression(let text):
return String(format: lang.tr("sim_notice_invalid_expression_fmt"), text)
case .unknownVariable(let name):
return String(format: lang.tr("sim_notice_unknown_variable_fmt"), name)
case .divisionByZero:
return lang.tr("sim_notice_division_by_zero")
case .wholeNumberExpected:
return lang.tr("sim_notice_whole_number_expected")
case .numberExpected:
return lang.tr("sim_notice_number_expected")
case .other(let text):
return text
}
}
}
Loading
Loading