Skip to content

Commit af1c312

Browse files
authored
Merge Upstream
2 parents a9006bd + 8d75c17 commit af1c312

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+7056
-1885
lines changed

.github/workflows/swift.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ jobs:
1818
- uses: actions/setup-node@v4
1919
with:
2020
node-version: 25-nightly
21-
- uses: swift-actions/setup-swift@v2
21+
22+
# Is it failing to run tests b/c we're overriding
23+
# what swift binary to use?
24+
- name: Setup Swift for Ubuntu
25+
if: runner.os == 'Linux'
26+
uses: swift-actions/setup-swift@v2
2227
with:
2328
swift-version: "6.0.3"
24-
- name: Swift Version
25-
run: swift --version
29+
2630
- uses: actions/checkout@v2
31+
2732
- name: Build
2833
run: swift build -v
34+
2935
- name: Run tests
3036
run: swift test -v

Package.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.7
22
//
33
// Copyright 2019 Google LLC
44
//
@@ -19,7 +19,7 @@ import PackageDescription
1919
let package = Package(
2020
name: "Fuzzilli",
2121
platforms: [
22-
.macOS(.v11),
22+
.macOS(.v13),
2323
],
2424
products: [
2525
.library(name: "Fuzzilli",targets: ["Fuzzilli"]),
@@ -28,6 +28,10 @@ let package = Package(
2828
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"),
2929
.package(url: "https://github.com/swift-server/RediStack.git", from: "1.4.1"),
3030
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
31+
.package(
32+
url: "https://github.com/apple/swift-collections.git",
33+
.upToNextMinor(from: "1.2.0")
34+
),
3135
],
3236
targets: [
3337
.target(name: "libsocket",
@@ -46,6 +50,7 @@ let package = Package(
4650
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
4751
.product(name: "NIO", package: "swift-nio"),
4852
.product(name: "RediStack", package: "RediStack"),
53+
.product(name: "Collections", package: "swift-collections"),
4954
"libsocket",
5055
"libreprl",
5156
"libcoverage"],
@@ -60,13 +65,13 @@ let package = Package(
6065
.copy("Protobuf/ast.proto"),
6166
.copy("Compiler/Parser")]),
6267

63-
.target(name: "REPRLRun",
68+
.executableTarget(name: "REPRLRun",
6469
dependencies: ["libreprl"]),
6570

66-
.target(name: "FuzzilliCli",
71+
.executableTarget(name: "FuzzilliCli",
6772
dependencies: ["Fuzzilli"]),
6873

69-
.target(name: "FuzzILTool",
74+
.executableTarget(name: "FuzzILTool",
7075
dependencies: ["Fuzzilli"]),
7176

7277
.testTarget(name: "FuzzilliTests",
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Collections
16+
17+
public class ContextGraph {
18+
// This is an edge, it holds all Generators that provide the `to` context at some point.
19+
// Another invariant is that each Generator will keep the original context, i.e. it will return to the `from` conetxt.
20+
struct EdgeKey: Hashable {
21+
let from: Context
22+
let to: Context
23+
24+
public init(from: Context, to: Context) {
25+
self.from = from
26+
self.to = to
27+
}
28+
}
29+
30+
// This struct describes the value of an edge in this ContextGraph.
31+
// It holds all `CodeGenerator`s that go from one context to another in a direct transition.
32+
public struct GeneratorEdge {
33+
var generators: [CodeGenerator] = []
34+
35+
// Adds a generator to this Edge.
36+
public mutating func addGenerator(_ generator: CodeGenerator) {
37+
generators.append(generator)
38+
}
39+
}
40+
41+
// This is a Path that goes from one Context to another via (usually more than one) `GeneratorEdge`. It is a full path in the Graph.
42+
// Every Edge in a path may be provided by various CodeGenerators.
43+
public struct Path {
44+
let edges: [GeneratorEdge]
45+
46+
// For each edge, pick a random Generator that provides that edge.
47+
public func randomConcretePath() -> [CodeGenerator] {
48+
edges.map { edge in
49+
chooseUniform(from: edge.generators)
50+
}
51+
}
52+
}
53+
54+
// This is the Graph, each pair of from and to, maps to a `GeneratorEdge`.
55+
var edges: [EdgeKey: GeneratorEdge] = [:]
56+
57+
public init(for generators: WeightedList<CodeGenerator>, withLogger logger: Logger) {
58+
// Technically we don't need any generator to emit the .javascript context, as this is provided by the toplevel.
59+
var providedContexts = Set<Context>([.javascript])
60+
var requiredContexts = Set<Context>()
61+
62+
for generator in generators {
63+
generator.providedContexts.forEach { ctx in
64+
providedContexts.insert(ctx)
65+
}
66+
67+
requiredContexts.insert(generator.requiredContext)
68+
}
69+
70+
// Check that every part that provides something is used by the next part of the Generator. This is a simple consistency check.
71+
for generator in generators where generator.parts.count > 1 {
72+
var currentContext = Context(generator.parts[0].providedContext)
73+
74+
for i in 1..<generator.parts.count {
75+
let stub = generator.parts[i]
76+
77+
guard stub.requiredContext.matches(currentContext) ||
78+
(stub.requiredContext.isJavascript && currentContext.isEmpty) ||
79+
// If the requiredContext is more than two, we should never provide a context.
80+
// See `CodeGenerator` for details.
81+
(!stub.requiredContext.isSingle && currentContext.isEmpty) else {
82+
fatalError("Inconsistent requires/provides Contexts for \(generator.name)")
83+
}
84+
85+
currentContext = Context(stub.providedContext)
86+
}
87+
}
88+
89+
for generator in generators {
90+
// Now check which generators don't have providers
91+
if !providedContexts.contains(generator.requiredContext) {
92+
logger.warning("Generator \(generator.name) cannot be run as it doesn't have a Generator that can provide this context.")
93+
94+
}
95+
96+
// All provided contexts must be required by some generator.
97+
if !generator.providedContexts.allSatisfy({
98+
requiredContexts.contains($0)
99+
}) {
100+
logger.warning("Generator \(generator.name) provides a context that is never required by another generator \(generator.providedContexts)")
101+
}
102+
}
103+
104+
// One can still try to build in a context that doesn't have generators, this will be caught in the build function, if we fail to find any suitable generator.
105+
// Otherwise we could assert here that the sets are equal.
106+
// One example is a GeneratorStub that opens a context, then calls build manually without it being split into two stubs where we have a yield point to assemble a synthetic generator.
107+
self.edges = [:]
108+
109+
for generator in generators {
110+
for providableContext in generator.providedContexts {
111+
let edge = EdgeKey(from: generator.requiredContext, to: providableContext)
112+
self.edges[edge, default: GeneratorEdge()].addGenerator(generator)
113+
}
114+
}
115+
}
116+
117+
/// Gets all possible paths from the `from` Context to the `to` Context.
118+
/// TODO: Do this initially and cache all results?
119+
func getAllPaths(from src: Context, to dst: Context) -> [[EdgeKey]] {
120+
// Do simple BFS to find all possible paths.
121+
var queue: Deque<[Context]> = [[src]]
122+
var paths: [[Context]] = []
123+
var seenNodes = Set<Context>([src])
124+
125+
while !queue.isEmpty {
126+
// use popFirst here from the deque.
127+
let currentPath = queue.popFirst()!
128+
129+
let currentNode = currentPath.last!
130+
131+
if currentNode == dst {
132+
paths.append(currentPath)
133+
}
134+
135+
// Get all possible edges from here on and push all of those to the queue.
136+
for edge in self.edges where edge.key.from == currentNode && !seenNodes.contains(edge.key.to) {
137+
// Prevent cycles, we don't care about complicated paths, but rather simple direct paths.
138+
seenNodes.insert(edge.key.to)
139+
queue.append(currentPath + [edge.key.to])
140+
}
141+
}
142+
143+
if paths.isEmpty {
144+
return []
145+
}
146+
147+
// Reduce this to Edges structs that we can easily look up.
148+
var edgePaths: [[EdgeKey]] = []
149+
for path in paths {
150+
var edgePath: [EdgeKey] = []
151+
for i in 0..<(path.count - 1) {
152+
let edge = EdgeKey(from: path[i], to: path[i+1])
153+
edgePath.append(edge)
154+
}
155+
edgePaths.append(edgePath)
156+
}
157+
158+
return edgePaths
159+
}
160+
161+
func getGenerators(from src: Context, to dst: Context) -> GeneratorEdge? {
162+
self.edges[EdgeKey(from: src, to: dst)]
163+
}
164+
165+
// TODO(cffsmith) implement this to filter for generators that are actually reachable, we can use this to avoid picking a .javascript generator when we're in .wasmFunction context for example.
166+
// This is needed since we cannot close Contexts or go back in time yet. This essentially calculates all reachable destinations from `src`.
167+
// The caller can then use `fuzzer.codeGenerators.filter { $0.requiredContext.contains(<any reachable>) }` and pick a random one from that subset.
168+
func getReachableContexts(from src: Context) -> [Context] {
169+
// Do simple BFS to find all possible paths.
170+
var queue: Deque<[Context]> = [[src]]
171+
var paths: [[Context]] = []
172+
var seenNodes = Set<Context>([src])
173+
174+
while !queue.isEmpty {
175+
let currentPath = queue.popFirst()!
176+
177+
let currentNode = currentPath.last!
178+
179+
var stillExploring = false
180+
181+
// Get all possible edges from here on and push all of those to the queue.
182+
for edge in self.edges where edge.key.from == currentNode && !seenNodes.contains(edge.key.to) {
183+
// Prevent cycles, we don't care about complicated paths, but rather simple direct paths.
184+
stillExploring = true
185+
seenNodes.insert(edge.key.to)
186+
queue.append(currentPath + [edge.key.to])
187+
}
188+
189+
// If we haven't added another node, it means we have found an "end".
190+
if !stillExploring {
191+
paths.append(currentPath)
192+
}
193+
}
194+
195+
if paths.isEmpty {
196+
return []
197+
}
198+
199+
// Map to all reachable contexts. so all that are on the path.
200+
let contextSet = paths.reduce(Set()) { res, path in
201+
res.union(path)
202+
}
203+
return Array(contextSet)
204+
}
205+
206+
// The return value is a list of possible Paths.
207+
// A Path is a possible way from one context to another.
208+
public func getCodeGeneratorPaths(from src: Context, to dst: Context) -> [Path]? {
209+
210+
let paths = getAllPaths(from: src, to: dst)
211+
212+
if paths.isEmpty {
213+
return nil
214+
}
215+
216+
return paths.map { edges in
217+
Path(edges: edges.map { edge in
218+
self.edges[edge]!
219+
})
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)