Skip to content

Commit e0e8812

Browse files
Merge pull request #9 from code4teaching/simulator-conditions
Simulator: string comparisons, parentheses, ! and stop on error
2 parents 4208ac8 + 3bd8f81 commit e0e8812

3 files changed

Lines changed: 303 additions & 31 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is loosely inspired by Keep a Changelog.
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- **Simulation:** conditions now support string comparisons (`name == "Anna"`, `name.equals("Anna")`), parentheses (`(a < 3 || a > 10) && b`), and negation with `!`. Operators inside string literals are no longer treated as operators.
12+
- **Simulation:** after an error the simulation stops on the failing block instead of skipping it and running on; Reset starts over.
13+
914
### Planned / Notes
1015

1116
- Documentation polish for product/marketing surfaces only when explicitly requested.

‎VisuStruct-iOS/VisuStructCore/Sources/VisuStructCore/VisuStructSimulationEngine.swift‎

Lines changed: 199 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public struct VisuStructSimulationState: Equatable, Sendable {
105105
public var inputError: String?
106106
public var lastTrace: String?
107107
public var message: String?
108+
/// Nach einem Fehler bleibt die Simulation auf dem fehlerhaften Schritt stehen, bis sie zurückgesetzt wird.
109+
public var isHalted: Bool
108110

109111
public init(
110112
stepIndex: Int = 0,
@@ -113,7 +115,8 @@ public struct VisuStructSimulationState: Equatable, Sendable {
113115
inputRequest: VisuStructSimulationInputRequest? = nil,
114116
inputError: String? = nil,
115117
lastTrace: String? = nil,
116-
message: String? = nil
118+
message: String? = nil,
119+
isHalted: Bool = false
117120
) {
118121
self.stepIndex = stepIndex
119122
self.variables = variables
@@ -122,6 +125,7 @@ public struct VisuStructSimulationState: Equatable, Sendable {
122125
self.inputError = inputError
123126
self.lastTrace = lastTrace
124127
self.message = message
128+
self.isHalted = isHalted
125129
}
126130
}
127131

@@ -135,7 +139,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
135139
}
136140

137141
public var canStep: Bool {
138-
state.inputRequest == nil && state.stepIndex < steps.count
142+
!state.isHalted && state.inputRequest == nil && state.stepIndex < steps.count
139143
}
140144

141145
public init(document: VisuStructDocument) {
@@ -152,7 +156,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
152156

153157
public mutating func step() {
154158
guard canStep else {
155-
if state.inputRequest == nil {
159+
if state.inputRequest == nil && !state.isHalted {
156160
state.message = "Simulation finished."
157161
}
158162
return
@@ -199,7 +203,7 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
199203
state.message = state.stepIndex >= steps.count ? "Simulation finished." : nil
200204
} catch {
201205
state.message = error.localizedDescription
202-
state.stepIndex += 1
206+
state.isHalted = true
203207
}
204208
}
205209

@@ -928,49 +932,93 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
928932
return try parser.parseDouble()
929933
}
930934

935+
/// Bedingungen wie in Java: `||` vor `&&` vor `!`, Klammern, Vergleiche von Zahlen und Strings, `a.equals(b)`.
931936
private func evaluateBoolExpression(_ expression: String) throws -> Bool {
932937
let trimmed = expression.trimmingCharacters(in: .whitespacesAndNewlines)
933938
if trimmed == "true" { return true }
934939
if trimmed == "false" { return false }
935940

936-
let orParts = splitBoolExpression(trimmed, separator: "||")
941+
let orParts = Self.splitTopLevel(trimmed, separator: "||")
937942
if orParts.count > 1 {
938943
for part in orParts where try evaluateBoolExpression(part) {
939944
return true
940945
}
941946
return false
942947
}
943948

944-
let andParts = splitBoolExpression(trimmed, separator: "&&")
949+
let andParts = Self.splitTopLevel(trimmed, separator: "&&")
945950
if andParts.count > 1 {
946951
for part in andParts where try !evaluateBoolExpression(part) {
947952
return false
948953
}
949954
return true
950955
}
951956

952-
let operators = [">=", "<=", "==", "!=", ">", "<"]
953-
for op in operators {
954-
if let range = trimmed.range(of: op) {
955-
let lhs = String(trimmed[..<range.lowerBound])
956-
let rhs = String(trimmed[range.upperBound...])
957-
let leftValue = try evaluateDoubleExpression(lhs)
958-
let rightValue = try evaluateDoubleExpression(rhs)
959-
switch op {
960-
case ">=": return leftValue >= rightValue
961-
case "<=": return leftValue <= rightValue
962-
case "==": return leftValue == rightValue
963-
case "!=": return leftValue != rightValue
964-
case ">": return leftValue > rightValue
965-
case "<": return leftValue < rightValue
966-
default: break
967-
}
968-
}
957+
if trimmed.hasPrefix("!"), !trimmed.hasPrefix("!=") {
958+
return try !evaluateBoolExpression(String(trimmed.dropFirst()))
959+
}
960+
961+
if let inner = Self.innerOfOuterParentheses(trimmed) {
962+
return try evaluateBoolExpression(inner)
963+
}
964+
965+
if let call = Self.parseEqualsCall(trimmed) {
966+
return try evaluateComparison(lhs: call.lhs, op: "==", rhs: call.rhs)
967+
}
968+
969+
if let comparison = Self.splitComparison(trimmed) {
970+
return try evaluateComparison(lhs: comparison.lhs, op: comparison.op, rhs: comparison.rhs)
969971
}
970972

973+
if try stringOperand(trimmed) != nil {
974+
throw SimulationError.invalidExpression(trimmed)
975+
}
971976
return try evaluateDoubleExpression(trimmed) != 0
972977
}
973978

979+
private func evaluateComparison(lhs: String, op: String, rhs: String) throws -> Bool {
980+
let leftString = try stringOperand(lhs)
981+
let rightString = try stringOperand(rhs)
982+
if leftString != nil || rightString != nil {
983+
guard let left = leftString, let right = rightString else {
984+
throw SimulationError.invalidExpression("\(lhs) \(op) \(rhs)")
985+
}
986+
switch op {
987+
case "==": return left == right
988+
case "!=": return left != right
989+
case "<": return left < right
990+
case "<=": return left <= right
991+
case ">": return left > right
992+
case ">=": return left >= right
993+
default: throw SimulationError.invalidExpression("\(lhs) \(op) \(rhs)")
994+
}
995+
}
996+
997+
let leftValue = try evaluateDoubleExpression(lhs)
998+
let rightValue = try evaluateDoubleExpression(rhs)
999+
switch op {
1000+
case "==": return leftValue == rightValue
1001+
case "!=": return leftValue != rightValue
1002+
case "<": return leftValue < rightValue
1003+
case "<=": return leftValue <= rightValue
1004+
case ">": return leftValue > rightValue
1005+
case ">=": return leftValue >= rightValue
1006+
default: throw SimulationError.invalidExpression("\(lhs) \(op) \(rhs)")
1007+
}
1008+
}
1009+
1010+
/// String-Literal oder String-Variable (auch Element eines String-Arrays); sonst `nil`.
1011+
private func stringOperand(_ text: String) throws -> String? {
1012+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
1013+
if let literal = Self.unquotedString(trimmed) {
1014+
return literal
1015+
}
1016+
if let value = try evaluateReference(trimmed), case .string(let string) = value {
1017+
return string
1018+
}
1019+
return nil
1020+
}
1021+
9741022
private enum SwitchSelectorValue: Equatable {
9751023
case int(Int)
9761024
case text(String)
@@ -1016,11 +1064,132 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
10161064
return nil
10171065
}
10181066

1019-
private func splitBoolExpression(_ expression: String, separator: String) -> [String] {
1020-
expression
1021-
.components(separatedBy: separator)
1022-
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
1023-
.filter { !$0.isEmpty }
1067+
/// Für jedes Zeichen: liegt es außerhalb von Klammern und String-Literalen?
1068+
private static func topLevelMask(_ chars: [Character]) -> [Bool] {
1069+
var mask = [Bool](repeating: false, count: chars.count)
1070+
var depth = 0
1071+
var inString = false
1072+
var escaped = false
1073+
for (index, char) in chars.enumerated() {
1074+
if inString {
1075+
if escaped {
1076+
escaped = false
1077+
} else if char == "\\" {
1078+
escaped = true
1079+
} else if char == "\"" {
1080+
inString = false
1081+
}
1082+
continue
1083+
}
1084+
switch char {
1085+
case "\"": inString = true
1086+
case "(", "[": depth += 1
1087+
case ")", "]": depth -= 1
1088+
default: mask[index] = depth == 0
1089+
}
1090+
}
1091+
return mask
1092+
}
1093+
1094+
private static func splitTopLevel(_ expression: String, separator: String) -> [String] {
1095+
let chars = Array(expression)
1096+
let separatorChars = Array(separator)
1097+
let mask = topLevelMask(chars)
1098+
var parts: [String] = []
1099+
var current: [Character] = []
1100+
var index = 0
1101+
while index < chars.count {
1102+
let end = index + separatorChars.count
1103+
if end <= chars.count, Array(chars[index..<end]) == separatorChars, mask[index..<end].allSatisfy({ $0 }) {
1104+
parts.append(String(current).trimmingCharacters(in: .whitespacesAndNewlines))
1105+
current = []
1106+
index = end
1107+
continue
1108+
}
1109+
current.append(chars[index])
1110+
index += 1
1111+
}
1112+
parts.append(String(current).trimmingCharacters(in: .whitespacesAndNewlines))
1113+
return parts
1114+
}
1115+
1116+
/// Erster Vergleichsoperator außerhalb von Klammern und Strings.
1117+
private static func splitComparison(_ expression: String) -> (lhs: String, op: String, rhs: String)? {
1118+
let chars = Array(expression)
1119+
let mask = topLevelMask(chars)
1120+
for index in chars.indices where mask[index] {
1121+
var op: String?
1122+
if index + 1 < chars.count, mask[index + 1] {
1123+
let pair = String(chars[index...index + 1])
1124+
if [">=", "<=", "==", "!="].contains(pair) {
1125+
op = pair
1126+
}
1127+
}
1128+
if op == nil, chars[index] == "<" || chars[index] == ">" {
1129+
op = String(chars[index])
1130+
}
1131+
guard let op else { continue }
1132+
let lhs = String(chars[..<index]).trimmingCharacters(in: .whitespacesAndNewlines)
1133+
let rhs = String(chars[(index + op.count)...]).trimmingCharacters(in: .whitespacesAndNewlines)
1134+
return (lhs, op, rhs)
1135+
}
1136+
return nil
1137+
}
1138+
1139+
/// Index der `)`, die die `(` bei `openIndex` schließt.
1140+
private static func matchingCloseParenthesis(in chars: [Character], from openIndex: Int) -> Int? {
1141+
var depth = 0
1142+
var inString = false
1143+
var escaped = false
1144+
for index in openIndex..<chars.count {
1145+
let char = chars[index]
1146+
if inString {
1147+
if escaped {
1148+
escaped = false
1149+
} else if char == "\\" {
1150+
escaped = true
1151+
} else if char == "\"" {
1152+
inString = false
1153+
}
1154+
continue
1155+
}
1156+
switch char {
1157+
case "\"": inString = true
1158+
case "(": depth += 1
1159+
case ")":
1160+
depth -= 1
1161+
if depth == 0 { return index }
1162+
default: break
1163+
}
1164+
}
1165+
return nil
1166+
}
1167+
1168+
/// `(a || b)` → `a || b`; `(a) && (b)` bleibt unverändert.
1169+
private static func innerOfOuterParentheses(_ text: String) -> String? {
1170+
let chars = Array(text)
1171+
guard chars.first == "(", chars.last == ")",
1172+
matchingCloseParenthesis(in: chars, from: 0) == chars.count - 1 else {
1173+
return nil
1174+
}
1175+
return String(chars[1..<(chars.count - 1)]).trimmingCharacters(in: .whitespacesAndNewlines)
1176+
}
1177+
1178+
/// Java-Stil `name.equals("Anna")`.
1179+
private static func parseEqualsCall(_ text: String) -> (lhs: String, rhs: String)? {
1180+
let chars = Array(text)
1181+
let marker = Array(".equals(")
1182+
guard chars.last == ")" else { return nil }
1183+
let mask = topLevelMask(chars)
1184+
for index in chars.indices where mask[index] && index + marker.count <= chars.count {
1185+
guard Array(chars[index..<(index + marker.count)]) == marker else { continue }
1186+
let openIndex = index + marker.count - 1
1187+
guard matchingCloseParenthesis(in: chars, from: openIndex) == chars.count - 1 else { continue }
1188+
let lhs = String(chars[..<index]).trimmingCharacters(in: .whitespacesAndNewlines)
1189+
let rhs = String(chars[(openIndex + 1)..<(chars.count - 1)]).trimmingCharacters(in: .whitespacesAndNewlines)
1190+
return (lhs, rhs)
1191+
}
1192+
return nil
10241193
}
10251194

10261195
private func renderNumericExpression(_ expression: String) throws -> String {
@@ -1160,9 +1329,8 @@ public struct VisuStructSimulationEngine: Equatable, Sendable {
11601329
continue
11611330
}
11621331
var stripped = String(afterKeyword).trimmingCharacters(in: .whitespacesAndNewlines)
1163-
if stripped.first == "(", stripped.last == ")" {
1164-
stripped = String(stripped.dropFirst().dropLast())
1165-
.trimmingCharacters(in: .whitespacesAndNewlines)
1332+
if let inner = innerOfOuterParentheses(stripped) {
1333+
stripped = inner
11661334
}
11671335
return stripped.isEmpty ? "true" : stripped
11681336
}

0 commit comments

Comments
 (0)