Skip to content
Closed
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 @@ -63,6 +63,7 @@ public struct SwiftViewInterpreter: Sendable {
onLargeStack {
let env = EvalEnvironment(values: state)
self.registerFunctions(program.file.statements, env)
self.bindTopLevelVariables(program.file.statements, env)
for item in program.file.statements {
if let expr = item.item.as(ExprSyntax.self), let node = self.evalView(expr, env) {
// A tripped node budget means the tree was truncated
Expand Down Expand Up @@ -114,6 +115,16 @@ public struct SwiftViewInterpreter: Sendable {
}
}

/// Binds file-scope variables into the root environment so user functions
/// can resolve constants declared outside their bodies.
private func bindTopLevelVariables(_ items: CodeBlockItemListSyntax, _ env: EvalEnvironment) {
for item in items {
if let decl = item.item.as(VariableDeclSyntax.self) {
applyBinding(decl, env)
}
}
}

private func bindParameters(_ decl: FunctionDeclSyntax, _ call: FunctionCallExprSyntax, _ env: EvalEnvironment) -> EvalEnvironment {
expressions.bindParameters(decl, call, env)
}
Expand Down Expand Up @@ -296,7 +307,7 @@ public struct SwiftViewInterpreter: Sendable {
// called in view position; evaluate its body as view items.
if let decl = env.lookupFunction(ref.baseName.text), let body = decl.body {
let scope = bindParameters(decl, call, env)
let nodes = evalItems(body.statements, scope)
let nodes = evalItemsResult(body.statements, scope).nodes
if nodes.count == 1 { return nodes[0] }
return RenderNode(kind: .vstack, children: nodes)
}
Expand All @@ -306,10 +317,28 @@ public struct SwiftViewInterpreter: Sendable {

// MARK: - ViewBuilder statements

/// The rendered nodes from a block and whether an explicit `return`
/// terminated the current function evaluation.
private enum ViewBlockResult {
case completed([RenderNode])
case returned([RenderNode])

var nodes: [RenderNode] {
switch self {
case let .completed(nodes), let .returned(nodes):
nodes
}
}
}

private func evalItems(_ items: CodeBlockItemListSyntax, _ env: EvalEnvironment) -> [RenderNode] {
evalItemsResult(items, env).nodes
}

private func evalItemsResult(_ items: CodeBlockItemListSyntax, _ env: EvalEnvironment) -> ViewBlockResult {
env.budget.enter()
defer { env.budget.leave() }
guard !env.budget.exceeded, !env.budget.nodesExceeded else { return [] }
guard !env.budget.exceeded, !env.budget.nodesExceeded else { return .completed([]) }
registerFunctions(items, env)
var out: [RenderNode] = []
for item in items {
Expand All @@ -320,21 +349,39 @@ public struct SwiftViewInterpreter: Sendable {
if let decl = node.as(VariableDeclSyntax.self) {
applyBinding(decl, env)
} else if let loop = node.as(ForStmtSyntax.self) {
out += evalFor(loop, env)
switch evalFor(loop, env) {
case let .completed(nodes):
out += nodes
case let .returned(nodes):
return .returned(nodes)
}
} else if let ifExpr = ifExpression(node) {
out += evalIf(ifExpr, env)
switch evalIf(ifExpr, env) {
case let .completed(nodes):
out += nodes
case let .returned(nodes):
return .returned(nodes)
}
} else if let switchExpr = switchExpression(node) {
out += evalSwitch(switchExpr, env)
switch evalSwitch(switchExpr, env) {
case let .completed(nodes):
out += nodes
case let .returned(nodes):
return .returned(nodes)
}
} else if let ret = node.as(ReturnStmtSyntax.self), let expr = ret.expression {
// A view helper with an explicit `return SomeView` (or
// `return ForEach(...) { }`) renders its returned expression,
// not nothing.
// not nothing, and exits the function instead of appending to
// the surrounding builder output.
var returned: [RenderNode] = []
if let call = expr.as(FunctionCallExprSyntax.self), isForEach(call) {
out += evalForEach(call, env)
returned = evalForEach(call, env)
} else if let child = evalView(expr, env) {
env.budget.recordNode()
out.append(child)
returned = [child]
}
return .returned(returned)
} else if let expr = node.as(ExprSyntax.self) {
if let call = expr.as(FunctionCallExprSyntax.self), isForEach(call) {
out += evalForEach(call, env)
Expand All @@ -344,7 +391,7 @@ public struct SwiftViewInterpreter: Sendable {
}
}
}
return out
return .completed(out)
}

/// Extracts an `if` from a code-block item, whether it appears directly
Expand Down Expand Up @@ -530,15 +577,15 @@ public struct SwiftViewInterpreter: Sendable {

/// Evaluates a view-position `switch`: the first matching (or `default`)
/// case's statements are rendered.
private func evalSwitch(_ switchExpr: SwitchExprSyntax, _ env: EvalEnvironment) -> [RenderNode] {
private func evalSwitch(_ switchExpr: SwitchExprSyntax, _ env: EvalEnvironment) -> ViewBlockResult {
let subject = expressions.eval(switchExpr.subject, env)
for caseSyntax in switchExpr.cases {
guard let switchCase = caseSyntax.as(SwitchCaseSyntax.self) else { continue }
if switchCaseMatches(switchCase.label, subject, env) {
return evalItems(switchCase.statements, env.makeChild())
return evalItemsResult(switchCase.statements, env.makeChild())
}
}
return []
return .completed([])
}

/// Whether a `switch` case label matches `subject` (literal/`.member`
Expand All @@ -561,35 +608,40 @@ public struct SwiftViewInterpreter: Sendable {
}
}

private func evalFor(_ loop: ForStmtSyntax, _ env: EvalEnvironment) -> [RenderNode] {
private func evalFor(_ loop: ForStmtSyntax, _ env: EvalEnvironment) -> ViewBlockResult {
guard let name = loop.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
let sequence = expressions.eval(loop.sequence, env),
let values = sequence.iterationValues else { return [] }
let values = sequence.iterationValues else { return .completed([]) }
var out: [RenderNode] = []
for value in values {
if env.budget.nodesExceeded { break } // same early-out as evalForEach
let scope = env.makeChild()
scope.define(name, value)
out += evalItems(loop.body.statements, scope)
switch evalItemsResult(loop.body.statements, scope) {
case let .completed(nodes):
out += nodes
case let .returned(nodes):
return .returned(nodes)
}
}
return out
return .completed(out)
}

private func evalIf(_ ifExpr: IfExprSyntax, _ env: EvalEnvironment) -> [RenderNode] {
private func evalIf(_ ifExpr: IfExprSyntax, _ env: EvalEnvironment) -> ViewBlockResult {
// The then-branch runs in a child scope so `if let x = …` bindings are
// visible to it.
let scope = env.makeChild()
if conditionsPass(ifExpr.conditions, scope) {
return evalItems(ifExpr.body.statements, scope)
return evalItemsResult(ifExpr.body.statements, scope)
}
guard let elseBody = ifExpr.elseBody else { return [] }
guard let elseBody = ifExpr.elseBody else { return .completed([]) }
if let block = elseBody.as(CodeBlockSyntax.self) {
return evalItems(block.statements, env.makeChild())
return evalItemsResult(block.statements, env.makeChild())
}
if let elseIf = elseBody.as(IfExprSyntax.self) {
return evalIf(elseIf, env)
}
return []
return .completed([])
}

/// Evaluates an `if`/`guard` condition list against `scope`, binding any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,32 @@ import Testing
#expect(node?.children.first?.children.first?.text == "one")
}

@Test func returnExitsViewFunctionBeforeFallthrough() {
let node = interp.evaluate("""
func badge(_ w) -> some View {
if w.unread == 0 { return AnyView(Text("EARLY")) }
return AnyView(Text("FALLTHROUGH"))
}
VStack { badge(w) }
""", state: [
"w": .object(["unread": .int(0)]),
])
#expect(node?.kind == .vstack)
#expect(node?.children.map(\.text) == ["EARLY"])
}

@Test func topLevelLetIsVisibleInsideFunction() {
let node = interp.evaluate("""
let MARK = "X:"
func label(_ v) -> String { return "\\(MARK)\\(v)" }
VStack { Text(label(s)) }
""", state: [
"s": .string("a-b-c"),
])
#expect(node?.kind == .vstack)
#expect(node?.children.map(\.text) == ["X:a-b-c"])
}

@Test func numberFormattedCurrencyAndReduce() {
let items = SwiftValue.array([
.object(["cost": .double(1.5)]),
Expand Down
Loading