Skip to content

Commit 9dc6ae4

Browse files
committed
fix(compiler): implement RangeLoop codegen and fix binary mode query defaults
- Add generateRangeLoop to LLVMCodeGenerator for 'for <var> from N to M' loops - Add RangeLoop to collectBoundVariablesFromStatement for proper variable tracking - Fix escapeJSON to escape all control characters (0x00-0x1F) including ESC (0x1B), preventing invalid JSON when ANSI escape sequences appear in string literals - Add _default_value_ binding in bindQueryModifiers for 'Retrieve ... default {...}' - Unbind _default_value_ before each statement to prevent stale fallback values
1 parent 0bb674d commit 9dc6ae4

2 files changed

Lines changed: 129 additions & 10 deletions

File tree

Sources/AROCompiler/LLVMC/LLVMCodeGenerator.swift

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ public final class LLVMCodeGenerator {
240240
generateMatchStatement(matchStatement, index: index, errorBlock: errorBlock)
241241
case let forEachLoop as ForEachLoop:
242242
generateForEachLoop(forEachLoop, index: index, errorBlock: errorBlock)
243+
case let rangeLoop as RangeLoop:
244+
generateRangeLoop(rangeLoop, index: index, errorBlock: errorBlock)
243245
case let whileLoop as WhileLoop:
244246
generateWhileLoop(whileLoop, index: index, errorBlock: errorBlock)
245247
case is BreakStatement:
@@ -301,6 +303,10 @@ public final class LLVMCodeGenerator {
301303
// Build object descriptor
302304
let objectDesc = buildObjectDescriptor(statement.object, prefix: prefix)
303305

306+
// Clear _default_value_ before binding modifiers (mirrors FeatureSetExecutor line 255)
307+
let defaultValueName = ctx.stringConstant("_default_value_")
308+
_ = ctx.module.insertCall(externals.variableUnbind, on: [ctx.currentContextVar!, defaultValueName], at: ctx.insertionPoint)
309+
304310
// Bind query modifiers if present
305311
bindQueryModifiers(statement.queryModifiers)
306312

@@ -585,6 +591,17 @@ public final class LLVMCodeGenerator {
585591
at: ip
586592
)
587593
}
594+
595+
// Bind default value if present (for optional retrieve with fallback)
596+
if let defaultExpr = modifiers.defaultValue {
597+
let defaultName = ctx.stringConstant("_default_value_")
598+
let defaultJSON = ctx.stringConstant(serializeExpression(defaultExpr))
599+
_ = ctx.module.insertCall(
600+
externals.evaluateAndBind,
601+
on: [ctx.currentContextVar!, defaultName, defaultJSON],
602+
at: ip
603+
)
604+
}
588605
}
589606

590607
private func bindRangeModifiers(_ modifiers: RangeModifiers) {
@@ -846,11 +863,21 @@ public final class LLVMCodeGenerator {
846863
}
847864

848865
private func escapeJSON(_ s: String) -> String {
849-
s.replacingOccurrences(of: "\\", with: "\\\\")
850-
.replacingOccurrences(of: "\"", with: "\\\"")
851-
.replacingOccurrences(of: "\n", with: "\\n")
852-
.replacingOccurrences(of: "\r", with: "\\r")
853-
.replacingOccurrences(of: "\t", with: "\\t")
866+
var result = ""
867+
for scalar in s.unicodeScalars {
868+
switch scalar.value {
869+
case 0x5C: result += "\\\\" // backslash
870+
case 0x22: result += "\\\"" // double quote
871+
case 0x0A: result += "\\n" // newline
872+
case 0x0D: result += "\\r" // carriage return
873+
case 0x09: result += "\\t" // tab
874+
case 0x00..<0x20: // other control characters (incl. ESC 0x1B)
875+
result += String(format: "\\u%04x", scalar.value)
876+
default:
877+
result += String(scalar)
878+
}
879+
}
880+
return result
854881
}
855882

856883
// MARK: - Control Flow (Stubs)
@@ -995,6 +1022,12 @@ public final class LLVMCodeGenerator {
9951022
for stmt in forEachLoop.body {
9961023
collectBoundVariablesFromStatement(stmt, into: &variables)
9971024
}
1025+
} else if let rangeLoop = statement as? RangeLoop {
1026+
// Range loop variable and body variables
1027+
variables.insert(rangeLoop.variable)
1028+
for stmt in rangeLoop.body {
1029+
collectBoundVariablesFromStatement(stmt, into: &variables)
1030+
}
9981031
} else if let whileLoop = statement as? WhileLoop {
9991032
// While loop variables are in the outer scope — collect them
10001033
for stmt in whileLoop.body {
@@ -1154,6 +1187,88 @@ public final class LLVMCodeGenerator {
11541187
ctx.setInsertionPoint(atEndOf: endBlock)
11551188
}
11561189

1190+
// MARK: - Range Loop Generation (for <var> from <low> to <high>)
1191+
1192+
private func generateRangeLoop(_ loop: RangeLoop, index: Int, errorBlock: BasicBlock) {
1193+
let prefix = "range\(index)"
1194+
1195+
// Create loop blocks
1196+
let condBlock = ctx.module.appendBlock(named: "\(prefix)_cond", to: ctx.currentFunction!)
1197+
let bodyBlock = ctx.module.appendBlock(named: "\(prefix)_body", to: ctx.currentFunction!)
1198+
let incrBlock = ctx.module.appendBlock(named: "\(prefix)_incr", to: ctx.currentFunction!)
1199+
let endBlock = ctx.module.appendBlock(named: "\(prefix)_end", to: ctx.currentFunction!)
1200+
1201+
// Evaluate `from` and `to` expressions into temp variables
1202+
let fromTempName = ctx.stringConstant("_rng_from_\(index)_")
1203+
let toTempName = ctx.stringConstant("_rng_to_\(index)_")
1204+
1205+
let fromJSON = ctx.stringConstant(serializeExpression(loop.from))
1206+
_ = ctx.module.insertCall(externals.evaluateAndBind, on: [ctx.currentContextVar!, fromTempName, fromJSON], at: ctx.insertionPoint)
1207+
1208+
let toJSON = ctx.stringConstant(serializeExpression(loop.to))
1209+
_ = ctx.module.insertCall(externals.evaluateAndBind, on: [ctx.currentContextVar!, toTempName, toJSON], at: ctx.insertionPoint)
1210+
1211+
// Extract integer values into stack-allocated i64 storage
1212+
let fromIntPtr = ctx.module.insertAlloca(ctx.i64Type, at: ctx.insertionPoint)
1213+
let toIntPtr = ctx.module.insertAlloca(ctx.i64Type, at: ctx.insertionPoint)
1214+
ctx.module.insertStore(ctx.i64Type.zero, to: fromIntPtr, at: ctx.insertionPoint)
1215+
ctx.module.insertStore(ctx.i64Type.zero, to: toIntPtr, at: ctx.insertionPoint)
1216+
_ = ctx.module.insertCall(externals.variableResolveInt, on: [ctx.currentContextVar!, fromTempName, fromIntPtr], at: ctx.insertionPoint)
1217+
_ = ctx.module.insertCall(externals.variableResolveInt, on: [ctx.currentContextVar!, toTempName, toIntPtr], at: ctx.insertionPoint)
1218+
1219+
let toInt = ctx.module.insertLoad(ctx.i64Type, from: toIntPtr, at: ctx.insertionPoint)
1220+
let fromInt = ctx.module.insertLoad(ctx.i64Type, from: fromIntPtr, at: ctx.insertionPoint)
1221+
1222+
// Allocate loop counter starting at fromInt
1223+
let counterPtr = ctx.module.insertAlloca(ctx.i64Type, at: ctx.insertionPoint)
1224+
ctx.module.insertStore(fromInt, to: counterPtr, at: ctx.insertionPoint)
1225+
1226+
// Jump to condition check
1227+
ctx.module.insertBr(to: condBlock, at: ctx.insertionPoint)
1228+
1229+
// === Condition Block: loop while counter < toInt ===
1230+
ctx.setInsertionPoint(atEndOf: condBlock)
1231+
let curCount = ctx.module.insertLoad(ctx.i64Type, from: counterPtr, at: ctx.insertionPoint)
1232+
let done = ctx.module.insertIntegerComparison(.sge, curCount, toInt, at: ctx.insertionPoint)
1233+
ctx.module.insertCondBr(if: done, then: endBlock, else: bodyBlock, at: ctx.insertionPoint)
1234+
1235+
// === Body Block ===
1236+
ctx.setInsertionPoint(atEndOf: bodyBlock)
1237+
1238+
let bodyCount = ctx.module.insertLoad(ctx.i64Type, from: counterPtr, at: ctx.insertionPoint)
1239+
1240+
// Bind loop variable as boxed integer (unbind first to allow rebinding)
1241+
let varNameStr = ctx.stringConstant(loop.variable)
1242+
_ = ctx.module.insertCall(externals.variableUnbind, on: [ctx.currentContextVar!, varNameStr], at: ctx.insertionPoint)
1243+
let varValue = ctx.module.insertCall(externals.valueCreateInt, on: [bodyCount], at: ctx.insertionPoint)
1244+
_ = ctx.module.insertCall(externals.variableBindValue, on: [ctx.currentContextVar!, varNameStr, varValue], at: ctx.insertionPoint)
1245+
1246+
// Unbind all body-bound variables to allow rebinding on each iteration
1247+
let bodyVars = collectBoundVariables(from: loop.body)
1248+
for varName in bodyVars where varName != loop.variable {
1249+
let bodyVarName = ctx.stringConstant(varName)
1250+
_ = ctx.module.insertCall(externals.variableUnbind, on: [ctx.currentContextVar!, bodyVarName], at: ctx.insertionPoint)
1251+
}
1252+
1253+
// Generate body statements
1254+
for (stmtIndex, stmt) in loop.body.enumerated() {
1255+
generateStatement(stmt, index: index * 100 + stmtIndex, errorBlock: incrBlock)
1256+
}
1257+
1258+
// Branch to increment
1259+
ctx.module.insertBr(to: incrBlock, at: ctx.insertionPoint)
1260+
1261+
// === Increment Block ===
1262+
ctx.setInsertionPoint(atEndOf: incrBlock)
1263+
let nextCount = ctx.module.insertLoad(ctx.i64Type, from: counterPtr, at: ctx.insertionPoint)
1264+
let incremented = ctx.module.insertAdd(nextCount, ctx.i64Type.constant(1), at: ctx.insertionPoint)
1265+
ctx.module.insertStore(incremented, to: counterPtr, at: ctx.insertionPoint)
1266+
ctx.module.insertBr(to: condBlock, at: ctx.insertionPoint)
1267+
1268+
// === End Block ===
1269+
ctx.setInsertionPoint(atEndOf: endBlock)
1270+
}
1271+
11571272
// MARK: - While Loop Generation (ARO-0131)
11581273

11591274
private func generateWhileLoop(_ loop: WhileLoop, index: Int, errorBlock: BasicBlock) {

Sources/ARORuntime/Bridge/RuntimeBridge.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ final class AROCRuntimeHandle: @unchecked Sendable {
5656

5757
/// Keyboard service shared across all contexts — must outlive individual handler contexts
5858
let keyboardService: KeyboardService
59+
/// Terminal service shared across all contexts — section state must persist between renders
60+
let terminalService: TerminalService?
5961
#endif
6062

6163
init() {
6264
self.runtime = Runtime()
6365
#if !os(Windows)
6466
self.keyboardService = KeyboardService(eventBus: .shared)
67+
self.terminalService = isatty(STDOUT_FILENO) != 0 ? TerminalService() : nil
6568
#endif
6669
// Event loop creation deferred to lazy var - no eager init needed
6770
}
@@ -146,11 +149,12 @@ class AROCContextHandle {
146149
self.templateService = ts
147150

148151
// Register terminal service for TTY output (ARO-0052)
149-
// ClearAction, RenderAction, ShowAction all require this service
150-
if isatty(STDOUT_FILENO) != 0 {
151-
let terminal = TerminalService()
152-
self.context.register(terminal)
153-
self.terminalService = terminal
152+
// ClearAction, RenderAction, ShowAction all require this service.
153+
// Use the shared instance from AROCRuntimeHandle so section state
154+
// persists across observer invocations (each gets a fresh context handle).
155+
if let sharedTerminal = runtime.terminalService {
156+
self.context.register(sharedTerminal)
157+
self.terminalService = sharedTerminal
154158
} else {
155159
self.terminalService = nil
156160
}

0 commit comments

Comments
 (0)