-
Notifications
You must be signed in to change notification settings - Fork 305
/
ProgramBuilder.swift
2714 lines (2325 loc) · 127 KB
/
ProgramBuilder.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Builds programs.
///
/// This provides methods for constructing and appending random
/// instances of the different kinds of operations in a program.
public class ProgramBuilder {
/// The fuzzer instance for which this builder is active.
public let fuzzer: Fuzzer
private let logger = Logger(withLabel: "ProgramBuilder")
/// The code and type information of the program that is being constructed.
private var code = Code()
/// Comments for the program that is being constructed.
private var comments = ProgramComments()
/// Every code generator that contributed to the current program.
private var contributors = Contributors()
/// The parent program for the program being constructed.
private let parent: Program?
public var context: Context {
return contextAnalyzer.context
}
/// If true, the variables containing a function is hidden inside the function's body.
///
/// For example, in
///
/// let f = b.buildPlainFunction(with: .parameters(n: 2) { args in
/// // ...
/// }
/// b.callFunction(f, withArgs: b.randomArguments(forCalling: f))
///
/// The variable f would *not* be visible inside the body of the plain function during building
/// when this is enabled. However, the variable will be visible during future mutations, it is only
/// hidden when the function is initially created.
///
/// The same is done for class definitions, which may also cause trivial recursion in the constructor,
/// but where usage of the output inside the class definition's body may also cause other problems,
/// for example since `class C { [C] = 42; }` is invalid.
///
/// This can make sense for a number of reasons. First, it prevents trivial recursion where a
/// function directly calls itself. Second, it prevents weird code like for example the following:
///
/// function f1() {
/// let o6 = { x: foo + foo, y() { return foo; } };
/// }
///
/// From being generated, which can happen quite frequently during prefix generation as
/// the number of visible variables may be quite small.
public let enableRecursionGuard = true
/// Counter to quickly determine the next free variable.
private var numVariables = 0
/// Context analyzer to keep track of the currently active IL context.
private var contextAnalyzer = ContextAnalyzer()
/// Visible variables management.
/// The `scopes` stack contains one entry per currently open scope containing all variables created in that scope.
private var scopes = Stack<[Variable]>([[]])
/// The `variablesInScope` array simply contains all variables that are currently in scope. It is effectively the `scopes` stack flattened.
private var variablesInScope = [Variable]()
/// Keeps track of variables that have explicitly been hidden and so should not be
/// returned from e.g. `randomVariable()`. See `hide()` for more details.
private var hiddenVariables = VariableSet()
private var numberOfHiddenVariables = 0
/// Type inference for JavaScript variables.
private var jsTyper: JSTyper
/// Argument generation budget.
/// This budget is used in `findOrGenerateArguments(forSignature)` and tracks the upper limit of variables that that function should emit.
/// If that upper limit is reached the function will stop generating new variables and use existing ones instead.
/// If this value is set to nil, there is no argument generation happening, every argument generation should enter the recursive function (findOrGenerateArgumentsInternal) through the public non-internal one.
private var argumentGenerationVariableBudget: Int? = nil
/// This is the top most signature that was requested when `findOrGeneratorArguments(forSignature)` was called, this is helpful for debugging.
private var argumentGenerationSignature: Signature? = nil
/// Stack of active object literals.
///
/// This needs to be a stack as object literals can be nested, for example if an object
/// literals is created inside a method/getter/setter of another object literals.
private var activeObjectLiterals = Stack<ObjectLiteral>()
/// When building object literals, the state for the current literal is exposed through this member and
/// can be used to add fields to the literal or to determine if some field already exists.
public var currentObjectLiteral: ObjectLiteral {
return activeObjectLiterals.top
}
/// Stack of active class definitions.
///
/// Similar to object literals, class definitions can be nested so this needs to be a stack.
private var activeClassDefinitions = Stack<ClassDefinition>()
/// When building class definitions, the state for the current definition is exposed through this member and
/// can be used to add fields to the class or to determine if some field already exists.
public var currentClassDefinition: ClassDefinition {
return activeClassDefinitions.top
}
/// Stack of active switch blocks.
private var activeSwitchBlocks = Stack<SwitchBlock>()
/// When building switch blocks, the state for the current switch block is exposed through this
/// member and can be used to add cases to the switch.
public var currentSwitchBlock: SwitchBlock {
return activeSwitchBlocks.top
}
/// How many variables are currently in scope.
public var numberOfVisibleVariables: Int {
assert(numberOfHiddenVariables <= variablesInScope.count)
return variablesInScope.count - numberOfHiddenVariables
}
/// Whether there are any variables currently in scope.
public var hasVisibleVariables: Bool {
return numberOfVisibleVariables > 0
}
/// All currently visible variables.
public var visibleVariables: [Variable] {
if numberOfHiddenVariables == 0 {
// Fast path for the common case.
return variablesInScope
} else {
return variablesInScope.filter({ !hiddenVariables.contains($0) })
}
}
/// Constructs a new program builder for the given fuzzer.
init(for fuzzer: Fuzzer, parent: Program?) {
self.fuzzer = fuzzer
self.jsTyper = JSTyper(for: fuzzer.environment)
self.parent = parent
}
/// Resets this builder.
public func reset() {
code.removeAll()
comments.removeAll()
contributors.removeAll()
numVariables = 0
scopes = Stack([[]])
variablesInScope.removeAll()
hiddenVariables.removeAll()
numberOfHiddenVariables = 0
contextAnalyzer = ContextAnalyzer()
jsTyper.reset()
activeObjectLiterals.removeAll()
activeClassDefinitions.removeAll()
}
/// Finalizes and returns the constructed program, then resets this builder so it can be reused for building another program.
public func finalize() -> Program {
let program = Program(code: code, parent: parent, comments: comments, contributors: contributors)
reset()
return program
}
/// Prints the current program as FuzzIL code to stdout. Useful for debugging.
public func dumpCurrentProgram() {
print(FuzzILLifter().lift(code))
}
/// Returns the current number of instructions of the program we're building.
public var currentNumberOfInstructions: Int {
return code.count
}
/// Returns the index of the next instruction added to the program. This is equal to the current size of the program.
public func indexOfNextInstruction() -> Int {
return currentNumberOfInstructions
}
/// Returns the most recently added instruction.
public func lastInstruction() -> Instruction {
assert(currentNumberOfInstructions > 0)
return code.lastInstruction
}
/// Add a trace comment to the currently generated program at the current position.
/// This is only done if inspection is enabled.
public func trace(_ commentGenerator: @autoclosure () -> String) {
if fuzzer.config.enableInspection {
// Use an autoclosure here so that template strings are only evaluated when they are needed.
comments.add(commentGenerator(), at: .instruction(code.count))
}
}
/// Add a trace comment at the start of the currently generated program.
/// This is only done if history inspection is enabled.
public func traceHeader(_ commentGenerator: @autoclosure () -> String) {
if fuzzer.config.enableInspection {
comments.add(commentGenerator(), at: .header)
}
}
///
/// Methods to obtain random values to use in a FuzzIL program.
///
/// Returns a random integer value.
public func randomInt() -> Int64 {
if probability(0.5) {
return chooseUniform(from: self.fuzzer.environment.interestingIntegers)
} else {
return withEqualProbability({
Int64.random(in: -0x10...0x10)
}, {
Int64.random(in: -0x10000...0x10000)
}, {
Int64.random(in: Int64(Int32.min)...Int64(Int32.max))
})
}
}
/// Returns a random integer value suitable as size of for example an array.
/// The returned value is guaranteed to be positive.
public func randomSize(upTo maximum: Int64 = 0x100000000) -> Int64 {
assert(maximum >= 0x1000)
if probability(0.5) {
return chooseUniform(from: fuzzer.environment.interestingIntegers.filter({ $0 >= 0 && $0 <= maximum }))
} else {
return withEqualProbability({
Int64.random(in: 0...0x10)
}, {
Int64.random(in: 0...0x100)
}, {
Int64.random(in: 0...0x1000)
}, {
Int64.random(in: 0...maximum)
})
}
}
/// Returns a random integer value suitable as index.
public func randomIndex() -> Int64 {
// Prefer small, (usually) positive, indices.
if probability(0.33) {
return Int64.random(in: -2...10)
} else {
return randomSize()
}
}
/// Returns a random floating point value.
public func randomFloat() -> Double {
if probability(0.5) {
return chooseUniform(from: fuzzer.environment.interestingFloats)
} else {
return withEqualProbability({
Double.random(in: 0.0...1.0)
}, {
Double.random(in: -10.0...10.0)
}, {
Double.random(in: -1000.0...1000.0)
}, {
Double.random(in: -1000000.0...1000000.0)
}, {
// We cannot do Double.random(in: -Double.greatestFiniteMagnitude...Double.greatestFiniteMagnitude) here,
// presumably because that range is larger than what doubles can represent? So split the range in two.
if probability(0.5) {
return Double.random(in: -Double.greatestFiniteMagnitude...0)
} else {
return Double.random(in: 0...Double.greatestFiniteMagnitude)
}
})
}
}
/// Returns a random string value.
public func randomString() -> String {
return withEqualProbability({
self.randomPropertyName()
}, {
self.randomMethodName()
}, {
chooseUniform(from: self.fuzzer.environment.interestingStrings)
}, {
String(self.randomInt())
}, {
String.random(ofLength: Int.random(in: 1...5))
})
}
func randomRegExpPattern(compatibleWithFlags flags: RegExpFlags) -> String {
// Generate a "base" regexp
var regex = ""
let desiredLength = Int.random(in: 1...4)
while regex.count < desiredLength {
regex += withEqualProbability({
String.random(ofLength: 1)
}, {
// Pick from the available RegExp pattern, based on flags.
let candidates = self.fuzzer.environment.interestingRegExps.filter({ pattern, incompatibleFlags in flags.isDisjoint(with: incompatibleFlags) })
return chooseUniform(from: candidates).pattern
})
}
// Now optionally concatenate with another regexp
if probability(0.3) {
regex += randomRegExpPattern(compatibleWithFlags: flags)
}
// Or add a quantifier, if there is not already a quantifier in the last position.
if probability(0.2) && !self.fuzzer.environment.interestingRegExpQuantifiers.contains(String(regex.last!)) {
regex += chooseUniform(from: self.fuzzer.environment.interestingRegExpQuantifiers)
}
// Or wrap in brackets
if probability(0.1) {
withEqualProbability({
// optionally invert the character set
if probability(0.2) {
regex = "^" + regex
}
regex = "[" + regex + "]"
}, {
regex = "(" + regex + ")"
})
}
return regex
}
/// Returns a random regular expression pattern.
public func randomRegExpPatternAndFlags() -> (String, RegExpFlags) {
let flags = RegExpFlags.random()
return (randomRegExpPattern(compatibleWithFlags: flags), flags)
}
/// Returns the name of a random builtin.
public func randomBuiltin() -> String {
return chooseUniform(from: fuzzer.environment.builtins)
}
/// Returns a random builtin property name.
///
/// This will return a random name from the environment's list of builtin property names,
/// i.e. a property that exists on (at least) one builtin object type.
func randomBuiltinPropertyName() -> String {
return chooseUniform(from: fuzzer.environment.builtinProperties)
}
/// Returns a random custom property name.
///
/// This will select a random property from a (usually relatively small) set of custom property names defined by the environment.
///
/// This should generally be used in one of two situations:
/// 1. If a new property is added to an object.
/// In that case, we prefer to add properties with custom names (e.g. ".a", ".b") instead of properties
/// with names that exist in the environment (e.g. ".length", ".prototype"). This way, in the resulting code
/// it will be fairly clear when a builtin property is accessed vs. a custom one. It also increases the chances
/// of selecting an existing property when choosing a random property to access, see the next point.
/// 2. If we have no static type information about the object we're accessing.
/// In that case there is a higher chance of success when using the small set of custom property names
/// instead of the much larger set of all property names that exist in the environment (or something else).
public func randomCustomPropertyName() -> String {
return chooseUniform(from: fuzzer.environment.customProperties)
}
/// Returns either a builtin or a custom property name, with equal probability.
public func randomPropertyName() -> String {
return probability(0.5) ? randomBuiltinPropertyName() : randomCustomPropertyName()
}
/// Returns a random builtin method name.
///
/// This will return a random name from the environment's list of builtin method names,
/// i.e. a method that exists on (at least) one builtin object type.
public func randomBuiltinMethodName() -> String {
return chooseUniform(from: fuzzer.environment.builtinMethods)
}
/// Returns a random custom method name.
///
/// This will select a random method from a (usually relatively small) set of custom method names defined by the environment.
///
/// See the comment for randomCustomPropertyName() for when this should be used.
public func randomCustomMethodName() -> String {
return chooseUniform(from: fuzzer.environment.customMethods)
}
/// Returns either a builtin or a custom method name, with equal probability.
public func randomMethodName() -> String {
return probability(0.5) ? randomBuiltinMethodName() : randomCustomMethodName()
}
// Settings and constants controlling the behavior of randomParameters() below.
// This determines how many variables of a given type need to be visible before
// that type is considered a candidate for a parameter type. For example, if this
// is three, then we need at least three visible .integer variables before creating
// parameters of type .integer.
private let thresholdForUseAsParameter = 3
// The probability of using .anything as parameter type even though we have more specific alternatives.
// Doing this sometimes is probably beneficial so that completely random values are passed to the function.
// Future mutations, such as the ExplorationMutator can then figure out what to do with the parameters.
// Writable so it can be changed for tests.
var probabilityOfUsingAnythingAsParameterTypeIfAvoidable = 0.20
// Generate random parameters for a subroutine.
//
// This will attempt to find a parameter types for which at least a few variables of a compatible types are
// currently available to (potentially) later be used as arguments for calling the generated subroutine.
public func randomParameters(n wantedNumberOfParameters: Int? = nil) -> SubroutineDescriptor {
assert(probabilityOfUsingAnythingAsParameterTypeIfAvoidable >= 0 && probabilityOfUsingAnythingAsParameterTypeIfAvoidable <= 1)
// If the caller didn't specify how many parameters to generated, find an appropriate
// number of parameters based on how many variables are currently visible (and can
// therefore later be used as arguments for calling the new function).
let n: Int
if let requestedN = wantedNumberOfParameters {
assert(requestedN > 0)
n = requestedN
} else {
switch numberOfVisibleVariables {
case 0...1:
n = 0
case 2...5:
n = Int.random(in: 1...2)
default:
n = Int.random(in: 2...4)
}
}
// Find all types of which we currently have at least a few visible variables that we could later use as arguments.
// TODO: improve this code by using some kind of cache? That could then also be used for randomVariable(ofType:) etc.
var availableVariablesByType = [ILType: Int]()
for v in visibleVariables {
let t = type(of: v)
// TODO: should we also add this values to the buckets for supertypes (without this becoming O(n^2))?
// TODO: alternatively just check for some common union types, e.g. .number, .primitive, as long as these can be used meaningfully?
availableVariablesByType[t] = (availableVariablesByType[t] ?? 0) + 1
}
var candidates = Array(availableVariablesByType.filter({ k, v in v >= thresholdForUseAsParameter }).keys)
if candidates.isEmpty {
candidates.append(.anything)
}
var params = ParameterList()
for _ in 0..<n {
if probability(probabilityOfUsingAnythingAsParameterTypeIfAvoidable) {
params.append(.anything)
} else {
params.append(.plain(chooseUniform(from: candidates)))
}
}
// TODO: also generate rest parameters and maybe even optional ones sometimes?
return .parameters(params)
}
public func findOrGenerateArguments(forSignature signature: Signature, maxNumberOfVariablesToGenerate: Int = 100) -> [Variable] {
assert(context.contains(.javascript))
argumentGenerationVariableBudget = numVariables + maxNumberOfVariablesToGenerate
argumentGenerationSignature = signature
defer {
argumentGenerationVariableBudget = nil
argumentGenerationSignature = nil
}
return findOrGenerateArgumentsInternal(forSignature: signature)
}
private func findOrGenerateArgumentsInternal(forSignature: Signature) -> [Variable] {
var args: [Variable] = []
// This should be called whenever we have a type that has known information about its properties but we don't have a constructor for it.
// This can be the case for configuration objects, e.g. objects that can be passed into DOMAPIs.
func createObjectWithProperties(_ type: ILType) -> Variable {
assert(type.MayBe(.object()))
// Before we do any generation below, let's take into account that we already create a variable with this invocation, i.e. the createObject at the end.
// Therefore we need to decrease the budget here temporarily.
self.argumentGenerationVariableBudget! -= 1
// We defer the increase again, because at that point the variable is actually visible, i.e. `numVariables` was increased through the `createObject` call.
defer { self.argumentGenerationVariableBudget! += 1 }
var properties: [String: Variable] = [:]
for propertyName in type.properties {
// If we have an object that has a group, we should get a type here, otherwise if we don't have a group, we will get .anything.
let propType = fuzzer.environment.type(ofProperty: propertyName, on: type)
properties[propertyName] = generateType(propType)
}
return createObject(with: properties)
}
func createObjectWithGroup(type: ILType) -> Variable {
let group = type.group!
// We can be sure that we have such a builtin with a signature because the Environment checks this during initialization.
let signature = fuzzer.environment.type(ofBuiltin: group).signature!
let constructor = loadBuiltin(group)
let arguments = findOrGenerateArgumentsInternal(forSignature: signature)
let constructed = construct(constructor, withArgs: arguments)
return constructed
}
func generateType(_ type: ILType) -> Variable {
if probability(0.5) {
if let existingVariable = randomVariable(ofType: type) {
return existingVariable
}
}
if numVariables >= argumentGenerationVariableBudget! {
logger.warning("Reached variable generation limit in generateType for Signature: \(argumentGenerationSignature!), returning a random variable for use as type \(type).")
return randomVariable(forUseAs: type)
}
// We only need to check against all base types from TypeSystem.swift, this works because we use .MayBe
// TODO: Not sure how we should handle merge types, e.g. .string + .object(...).
let typeGenerators: [(ILType, () -> Variable)] = [
(.integer, { return self.loadInt(self.randomInt()) }),
(.string, { return self.loadString(self.randomString()) }),
(.boolean, { return self.loadBool(probability(0.5)) }),
(.bigint, { return self.loadBigInt(self.randomInt()) }),
(.float, { return self.loadFloat(self.randomFloat()) }),
(.regexp, {
let (pattern, flags) = self.randomRegExpPatternAndFlags()
return self.loadRegExp(pattern, flags)
}),
(.iterable, { return self.createArray(with: self.randomVariables(upTo: 5)) }),
(.function(), {
// TODO: We could technically generate a full function here but then we would enter the full code generation logic which could do anything.
// Because we want to avoid this, we will just pick anything that can be a function.
return self.randomVariable(forUseAs: .function())
}),
(.undefined, { return self.loadUndefined() }),
(.constructor(), {
// TODO: We have the same issue as above for functions.
return self.randomVariable(forUseAs: .constructor())
}),
(.object(), {
// If we have a group on this object and we have a builtin, that means we can construct it with new.
if let group = type.group, self.fuzzer.environment.hasBuiltin(group) && self.fuzzer.environment.type(ofBuiltin: group).Is(.constructor()) {
return createObjectWithGroup(type: type)
} else {
// Otherwise this is one of the following:
// 1. an object with more type information, i.e. it has a group, but no associated builtin, e.g. we cannot construct it with new.
// 2. an object without a group, but it has some required fields.
// In either case, we try to construct such an object.
return createObjectWithProperties(type)
}
})
]
// Make sure that we walk over these tests and their generators randomly.
// The requested type could be a Union of other types and as such we want to randomly generate one of them, therefore we also use the MayBe test below.
for (t, generate) in typeGenerators.shuffled() {
if type.MayBe(t) {
let variable = generate()
return variable
}
}
logger.warning("Type \(type) was not handled, returning random variable.")
return randomVariable(forUseAs: type)
}
outer: for parameter in forSignature.parameters {
switch parameter {
case .plain(let t):
args.append(generateType(t))
case .opt(let t):
if probability(0.5) {
args.append(generateType(t))
} else {
// We decided to not provide an optional parameter, so we can stop here.
break outer
}
case .rest(let t):
for _ in 0...Int.random(in: 1...3) {
args.append(generateType(t))
}
}
}
return args
}
///
/// Access to variables.
///
/// Returns a random variable.
public func randomVariable() -> Variable {
assert(hasVisibleVariables)
return findVariable()!
}
/// Returns up to N (different) random variables.
/// This method will only return fewer than N variables if the number of currently visible variables is less than N.
public func randomVariables(upTo n: Int) -> [Variable] {
guard hasVisibleVariables else { return [] }
var variables = [Variable]()
while variables.count < n {
guard let newVar = findVariable(satisfying: { !variables.contains($0) }) else {
break
}
variables.append(newVar)
}
return variables
}
/// Returns up to N potentially duplicate random variables.
public func randomVariables(n: Int) -> [Variable] {
assert(hasVisibleVariables)
return (0..<n).map { _ in randomVariable() }
}
/// This probability affects the behavior of `randomVariable(forUseAs:)`. In particular, it determines how much variables with
/// a known-to-be-matching type will be preferred over variables with a more general, or even unknown type. For example, if this is
/// 0.5, then 50% of the time we'll first try to find an exact match (`type(of: result).Is(requestedType)`) before trying the
/// more general search (`type(of: result).MayBe(requestedType)`) which also includes variables of unknown type.
/// This is writable for use in tests, but it could also be used to change how "conservative" variable selection is.
var probabilityOfVariableSelectionTryingToFindAnExactMatch = 0.5
/// This threshold affects the behavior of `randomVariable(forUseAs:)`. It determines how many existing variables of the
/// requested type we want to have before we try to find an exact match. If there are fewer variables of the requested type, we'll
/// always do a more general search which may also return variables of unknown (i.e. `.anything`) type.
/// This ensures that consecutive queries for the same type can return different variables.
let minVisibleVariablesOfRequestedTypeForVariableSelection = 3
/// Returns a random variable to be used as the given type.
///
/// This function may return variables of a different type, or variables that may have the requested type, but could also have a different type.
/// For example, when requesting a .integer, this function may also return a variable of type .number, .primitive, or even .anything as all of these
/// types may be an integer (but aren't guaranteed to be). In this way, this function ensures that variables for which no exact type could be statically
/// determined will also be used as inputs for following code.
///
/// It's the caller's responsibility to check the type of the returned variable to avoid runtime exceptions if necessary. For example, if performing a
/// property access, the returned variable should be checked if it `MayBe(.nullish)` in which case a property access would result in a
/// runtime exception and so should be appropriately guarded against that.
///
/// If the variable must be of the specified type, use `randomVariable(ofType:)` instead.
public func randomVariable(forUseAs type: ILType) -> Variable {
assert(type != .nothing)
var result: Variable? = nil
// Prefer variables that are known to have the requested type if there's a sufficient number of them.
if probability(probabilityOfVariableSelectionTryingToFindAnExactMatch) &&
haveAtLeastNVisibleVariables(ofType: type, n: minVisibleVariablesOfRequestedTypeForVariableSelection) {
result = findVariable(satisfying: { self.type(of: $0).Is(type) })
}
// Otherwise, select variables that may have the desired type, but could also be something else.
// In particular, this query will include all variable for which we don't know the type as they'll
// be typed as .anything. We usually expect to have a lot of candidates available for this query,
// so we don't check the number of them upfront as we do for the above query.
if result == nil {
result = findVariable(satisfying: { self.type(of: $0).MayBe(type) })
}
// Worst case fall back to completely random variables. This should happen rarely, as we'll usually have
// at least some variables of type .anything.
return result ?? randomVariable()
}
/// Returns a random variable that is known to have the given type.
///
/// This will return a variable for which `b.type(of: v).Is(type)` is true, i.e. for which our type inference
/// could prove that it will have the specified type. If no such variable is found, this function returns nil.
public func randomVariable(ofType type: ILType) -> Variable? {
assert(type != .nothing)
return findVariable(satisfying: { self.type(of: $0).Is(type) })
}
/// Returns a random variable that is not known to have the given type.
///
/// This will return a variable for which `b.type(of: v).Is(type)` is false, i.e. for which our type inference
/// could not prove that it has the given type. Note that this is different from a variable that is known not to have
/// the given type: this function can return variables for which `b.type(of: v).MayBe(type)` is true.
/// If no such variable is found, this function returns nil.
public func randomVariable(preferablyNotOfType type: ILType) -> Variable? {
return findVariable(satisfying: { !self.type(of: $0).Is(type) })
}
/// Returns a random variable satisfying the given constraints or nil if none is found.
func findVariable(satisfying filter: ((Variable) -> Bool) = { _ in true }) -> Variable? {
assert(hasVisibleVariables)
// TODO: we should implement some kind of fast lookup data structure to speed up the lookup of variables by type.
// We have to be careful though to correctly take type changes (e.g. from reassignments) into account.
// Also filter out any hidden variables.
var isIncluded = filter
if numberOfHiddenVariables != 0 {
isIncluded = { !self.hiddenVariables.contains($0) && filter($0) }
}
var candidates = [Variable]()
// Prefer the outputs of the last instruction to build longer data-flow chains.
if probability(0.15) {
candidates = Array(code.lastInstruction.allOutputs)
candidates = candidates.filter(isIncluded)
}
// Prefer inner scopes if we're not anyway using one of the newest variables.
let scopes = scopes
if candidates.isEmpty && probability(0.75) {
candidates = chooseBiased(from: scopes.elementsStartingAtBottom(), factor: 1.25)
candidates = candidates.filter(isIncluded)
}
// If we haven't found any candidates yet, take all visible variables into account.
if candidates.isEmpty {
candidates = variablesInScope.filter(isIncluded)
}
if candidates.isEmpty {
return nil
}
return chooseUniform(from: candidates)
}
/// Helper function to determine if we have a sufficient number of variables of a given type to ensure that
/// consecutive queries for a variable of a given type will not always return the same variables.
private func haveAtLeastNVisibleVariables(ofType t: ILType, n: Int) -> Bool {
var count = 0
for v in variablesInScope where !hiddenVariables.contains(v) && type(of: v).Is(t) {
count += 1
if count >= n {
return true
}
}
return false
}
/// Find random variables to use as arguments for calling the specified function.
///
/// This function will attempt to find variables that are compatible with the functions parameter types (if any). However,
/// if no matching variables can be found for a parameter, this function will fall back to using a random variable. It is
/// then the caller's responsibility to determine whether the function call can still be performed without raising a runtime
/// exception or if it needs to be guarded against that.
/// In this way, functions/methods for which no matching arguments currently exist can still be called (but potentially
/// wrapped in a try-catch), which then gives future mutations (in particular Mutators such as the ProbingMutator) the
/// chance to find appropriate arguments for the function.
public func randomArguments(forCalling function: Variable) -> [Variable] {
let signature = type(of: function).signature ?? Signature.forUnknownFunction
return randomArguments(forCallingFunctionWithSignature: signature)
}
/// Find random variables to use as arguments for calling the specified method.
///
/// See the comment above `randomArguments(forCalling function: Variable)` for caveats.
public func randomArguments(forCallingMethod methodName: String, on object: Variable) -> [Variable] {
let signature = methodSignature(of: methodName, on: object)
return randomArguments(forCallingFunctionWithSignature: signature)
}
/// Find random variables to use as arguments for calling the specified method.
///
/// See the comment above `randomArguments(forCalling function: Variable)` for caveats.
public func randomArguments(forCallingMethod methodName: String, on objType: ILType) -> [Variable] {
let signature = methodSignature(of: methodName, on: objType)
return randomArguments(forCallingFunctionWithSignature: signature)
}
/// Find random variables to use as arguments for calling a function with the specified signature.
///
/// See the comment above `randomArguments(forCalling function: Variable)` for caveats.
public func randomArguments(forCallingFunctionWithSignature signature: Signature) -> [Variable] {
return randomArguments(forCallingFunctionWithParameters: signature.parameters)
}
/// Find random variables to use as arguments for calling a function with the given parameters.
///
/// See the comment above `randomArguments(forCalling function: Variable)` for caveats.
public func randomArguments(forCallingFunctionWithParameters params: ParameterList) -> [Variable] {
assert(params.count == 0 || hasVisibleVariables)
let parameterTypes = prepareArgumentTypes(forParameters: params)
return parameterTypes.map({ randomVariable(forUseAs: $0) })
}
/// Find random arguments for a function call and spread some of them.
public func randomCallArgumentsWithSpreading(n: Int) -> (arguments: [Variable], spreads: [Bool]) {
var arguments: [Variable] = []
var spreads: [Bool] = []
for _ in 0...n {
let val = randomVariable()
arguments.append(val)
// Prefer to spread values that we know are iterable, as non-iterable values will lead to exceptions ("TypeError: Found non-callable @@iterator")
if type(of: val).Is(.iterable) {
spreads.append(probability(0.9))
} else {
spreads.append(probability(0.1))
}
}
return (arguments, spreads)
}
/// Hide the specified variable, preventing it from being used as input by subsequent code.
///
/// Hiding a variable prevents it from being returned from `randomVariable()` and related functions, which
/// in turn prevents it from being used as input for later instructions, unless the hidden variable is explicitly specified
/// as input, which is still allowed.
///
/// This can be useful for example if a CodeGenerator needs to create temporary values that should not be used
/// by any following code. It is also used to prevent trivial recursion by hiding the function variable inside its body.
public func hide(_ variable: Variable) {
assert(!hiddenVariables.contains(variable))
assert(visibleVariables.contains(variable))
hiddenVariables.insert(variable)
numberOfHiddenVariables += 1
}
/// Unhide the specified variable so that it can again be used as input by subsequent code.
///
/// The variable must have previously been hidden using `hide(variable:)` above.
public func unhide(_ variable: Variable) {
assert(numberOfHiddenVariables > 0)
assert(hiddenVariables.contains(variable))
assert(variablesInScope.contains(variable))
assert(!visibleVariables.contains(variable))
hiddenVariables.remove(variable)
numberOfHiddenVariables -= 1
}
/// Type information access.
public func type(of v: Variable) -> ILType {
return jsTyper.type(of: v)
}
/// Returns the type of the `super` binding at the current position.
public func currentSuperType() -> ILType {
return jsTyper.currentSuperType()
}
/// Returns the type of the super constructor.
public func currentSuperConstructorType() -> ILType {
return jsTyper.currentSuperConstructorType()
}
public func type(ofProperty property: String, on v: Variable) -> ILType {
return jsTyper.inferPropertyType(of: property, on: v)
}
public func methodSignature(of methodName: String, on object: Variable) -> Signature {
return jsTyper.inferMethodSignature(of: methodName, on: object)
}
public func methodSignature(of methodName: String, on objType: ILType) -> Signature {
return jsTyper.inferMethodSignature(of: methodName, on: objType)
}
/// Overwrite the current type of the given variable with a new type.
/// This can be useful if a certain code construct is guaranteed to produce a value of a specific type,
/// but where our static type inference cannot determine that.
public func setType(ofVariable variable: Variable, to variableType: ILType) {
jsTyper.setType(of: variable, to: variableType)
}
/// This helper function converts parameter types into argument types, for example by "unrolling" rest parameters and handling optional parameters.
private func prepareArgumentTypes(forParameters params: ParameterList) -> [ILType] {
var argumentTypes = [ILType]()
for param in params {
switch param {
case .rest(let t):
// "Unroll" the rest parameter
for _ in 0..<Int.random(in: 0...5) {
argumentTypes.append(t)
}
case .opt(let t):
// It's an optional argument, so stop here in some cases
if probability(0.25) {
return argumentTypes
}
fallthrough
case .plain(let t):
argumentTypes.append(t)
}
}
return argumentTypes
}
///
/// Adoption of variables from a different program.
/// Required when copying instructions between program.
///
private var varMaps = [VariableMap<Variable>]()
/// Prepare for adoption of variables from the given program.
///
/// This sets up a mapping for variables from the given program to the
/// currently constructed one to avoid collision of variable names.
public func beginAdoption(from program: Program) {
varMaps.append(VariableMap())
}
/// Finishes the most recently started adoption.
public func endAdoption() {
varMaps.removeLast()
}
/// Executes the given block after preparing for adoption from the provided program.
public func adopting(from program: Program, _ block: () -> Void) {
beginAdoption(from: program)
block()
endAdoption()
}
/// Maps a variable from the program that is currently configured for adoption into the program being constructed.
public func adopt(_ variable: Variable) -> Variable {
if !varMaps.last!.contains(variable) {
varMaps[varMaps.count - 1][variable] = nextVariable()
}
return varMaps.last![variable]!
}
/// Maps a list of variables from the program that is currently configured for adoption into the program being constructed.
public func adopt<Variables: Collection>(_ variables: Variables) -> [Variable] where Variables.Element == Variable {
return variables.map(adopt)
}
/// Adopts an instruction from the program that is currently configured for adoption into the program being constructed.
public func adopt(_ instr: Instruction) {
internalAppend(Instruction(instr.op, inouts: adopt(instr.inouts), flags: instr.flags))
}
/// Append an instruction at the current position.
public func append(_ instr: Instruction) {
for v in instr.allOutputs {
numVariables = max(v.number + 1, numVariables)
}
internalAppend(instr)
}
/// Append a program at the current position.
///
/// This also renames any variable used in the given program so all variables
/// from the appended program refer to the same values in the current program.
public func append(_ program: Program) {
adopting(from: program) {
for instr in program.code {
adopt(instr)
}
}
}
// Probabilities of remapping variables to host variables during splicing. These are writable so they can be reconfigured for testing.
// We use different probabilities for outer and for inner outputs: while we rarely want to replace outer outputs, we frequently want to replace inner outputs
// (e.g. function parameters) to avoid splicing function definitions that may then not be used at all. Instead, we prefer to splice only the body of such functions.
var probabilityOfRemappingAnInstructionsOutputsDuringSplicing = 0.10
var probabilityOfRemappingAnInstructionsInnerOutputsDuringSplicing = 0.75
// The probability of including an instruction that may mutate a variable required by the slice (but does not itself produce a required variable).
var probabilityOfIncludingAnInstructionThatMayMutateARequiredVariable = 0.5
/// Splice code from the given program into the current program.
///
/// Splicing computes a set of dependent (through dataflow) instructions in one program (called a "slice") and inserts it at the current position in this program.
///
/// If the optional index is specified, the slice starting at that instruction is used. Otherwise, a random slice is computed.
/// If mergeDataFlow is true, the dataflows of the two programs are potentially integrated by replacing some variables in the slice with "compatible" variables in the host program.
/// Returns true on success (if at least one instruction has been spliced), false otherwise.
@discardableResult
public func splice(from program: Program, at specifiedIndex: Int? = nil, mergeDataFlow: Bool = true) -> Bool {
// Splicing:
//