Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements compilation of new parameter types #449

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 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
70 changes: 65 additions & 5 deletions Sources/Fuzzilli/Compiler/Compiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,10 @@ public class JavaScriptCompiler {
try enterNewScope {
let beginCatch = emit(BeginCatch())
if tryStatement.catch.hasParameter {
map(tryStatement.catch.parameter.name, to: beginCatch.innerOutput)
guard case let .identifierParameter(identifier) = tryStatement.catch.parameter.parameter else {
throw CompilerError.unsupportedFeatureError("Only identifier parameters are supported in catch blocks")
}
map(identifier.name, to: beginCatch.innerOutput)
}
for statement in tryStatement.catch.body {
try compileStatement(statement)
Expand Down Expand Up @@ -1071,14 +1074,71 @@ public class JavaScriptCompiler {
}

private func mapParameters(_ parameters: [Compiler_Protobuf_Parameter], to variables: ArraySlice<Variable>) {
saelo marked this conversation as resolved.
Show resolved Hide resolved
assert(parameters.count == variables.count)
for (param, v) in zip(parameters, variables) {
map(param.name, to: v)
// Maps parameters of a function to variables that are used in that function's scope.
// func extractIdentifiers and var flatParameters help to process object and array patterns.
var flatParameters: [String] = []
func extractIdentifiers(from param: Compiler_Protobuf_Parameter) {
switch param.parameter {
case .identifierParameter(let identifier):
flatParameters.append(identifier.name)
case .objectParameter(let object):
for property in object.parameters {
extractIdentifiers(from: property.parameterValue)
}
case .arrayParameter(let array):
for element in array.elements {
extractIdentifiers(from: element)
}
case .none:
fatalError("Unexpected parameter type: .none in mapParameters")
}
}
for param in parameters {
extractIdentifiers(from: param)
}
assert(flatParameters.count == variables.count, "The number of variables (\(variables.count)) does not match the number of parameters (\(flatParameters.count)).")
for (name, v) in zip(flatParameters, variables) {
map(name, to: v)
}
}

private func convertParameters(_ parameters: [Compiler_Protobuf_Parameter]) -> Parameters {
return Parameters(count: parameters.count)
// Converts a protobuf signature to a FuzzIL signature.
var totalParameterCount = 0
var patterns = [ParameterPattern]()
func processParameter(_ param: Compiler_Protobuf_Parameter) -> ParameterPattern {
switch param.parameter {
case .identifierParameter(_):
totalParameterCount += 1
return .identifier
case .objectParameter(let object):
var properties = [ObjectPatternProperty]()
for property in object.parameters {
let key = property.parameterKey
let valuePattern = processParameter(property.parameterValue)
properties.append(ObjectPatternProperty(key: key, value: valuePattern))
}
return .object(properties: properties)
case .arrayParameter(let array):
var elements = [ParameterPattern]()
for element in array.elements {
let elementPattern = processParameter(element)
elements.append(elementPattern)
}
return .array(elements: elements)
case .none:
fatalError("Unexpected parameter type: .none in convertParameters")
}
}
for param in parameters {
let pattern = processParameter(param)
patterns.append(pattern)
}
var params = Parameters(
count: totalParameterCount
)
params.patterns = patterns
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can the constructor take patterns and then keep them immutable (i.e. as a let) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will work on this now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it, the more I'm starting to feel this might be a bigger problem.

Changing the constructor requires updating all places in the codebase where we instantiate Patterns. Even if we use a default value for backward compatibility, shouldn’t we still review every instance in the entire codebase where Parameters is referenced and ask: “What should be the patterns argument for the Parameters constructor?”

I've also noticed there are separate data structures, Parameter and ParameterList, which might also need revision—along with all their references in the codebase.

These extensive changes might be needed again once rest parameters and default values are supported.

The point I’m trying to make is that it feels like a lot of things might need to change for this, but the effort should also be justified and prioritized. I’m thinking it would make the most sense for me to focus on implementing the delete operator in Compilation and string object keys first, as they are likely easier to implement and will together enable hundreds of new seeds to become compilable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to working on delete operator and string object keys, those should allow for some quick wins!

I think it would make sense to have a convenience constructor of Parameters that creates a simple list of parameters: no patterns, no rest parameter, etc. just function foo(a1, a2, a3) {. Then we can have a second constructor for all the fancy stuff (patterns, rest param, etc.). I would think that the majority of use cases in Fuzzilli want the first constructor. Most of the time that should be the right thing to do. Only if you actually want to stress the logic for handling the special parameter types in the target engine would you want the other constructor. And that should then mostly be limited to the Compiler and a few CodeGenerators (or I guess the randomParameters() helper function).

Regarding the ParameterList, I think this one doesn't need to change, but maybe its documentation should be updated. The way I see it, there are basically two use cases for the types of a function's parameters:

  • The "outer" view: the argument types that a caller need to use, and
  • The "inner" view: the types of the parameters that the callee can use

The ParameterList struct (which is part of a Signature) is used for both of these cases. In both cases, we still only need a "flat" list of types: one for each argument or parameter. However, up until now, the two "views" would be identical: if the 2nd parameter was of type .integer, then that's what the caller had to pass and what the callee could assume. Now, for the caller the 2nd parameter might be of type .object(withProperties: ["a", "b"]) while for the callee, the 2nd parameter might be of type .integer (because it's something like function foo(p, {a: v3, b: v4}). So the same function can have different ParameterLists for the two "views". I think it's fine to keep using the same data structures for that, just the distinction should maybe be described in a comment? Alternatively, we could try to force the distinction by using two different data structures. I'm not sure that will work well though, but might be worth a try. WDYT?

return params
}

/// Convenience accessor for the currently active scope.
Expand Down
43 changes: 41 additions & 2 deletions Sources/Fuzzilli/Compiler/Parser/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,47 @@ function parse(script, proto) {
}

function visitParameter(param) {
assert(param.type == 'Identifier');
return make('Parameter', { name: param.name });
assert(['Identifier', 'ObjectPattern', 'ArrayPattern'].includes(param.type));
switch (param.type) {
case 'Identifier': {
return make('IdentifierParameter', { identifierParameter: { name: param.name } });
}
case 'ObjectPattern': {
const parameters = param.properties.map(property => {
assert(property.type === 'ObjectProperty');
assert(property.computed === false);
assert(property.method === false);
let parameterKey;
if (property.key.type === 'Identifier') {
parameterKey = property.key.name;
} else if (property.key.type === 'Literal') {
// Internally, literal keys are stored as strings. So we can convert them to strings here.
parameterKey = property.key.value.toString();
} else {
throw new Error('Unsupported property key type: ' + property.key.type);
}
const parameterValue = visitParameter(property.value);
return make('ObjectParameterProperty', {
parameterKey: parameterKey,
parameterValue: parameterValue
});
});
return make('ObjectParameter', { objectParameter: { parameters } });
}
case 'ArrayPattern': {
const elements = param.elements.map(element => {
if (element === null) {
throw new Error('Holes in array parameters are not supported');
} else {
return visitParameter(element);
}
});
return make('ArrayParameter', { arrayParameter: { elements } });
}
default: {
throw new Error('Unsupported parameter type: ' + param.type);
}
}
}

function visitVariableDeclaration(node) {
Expand Down
32 changes: 31 additions & 1 deletion Sources/Fuzzilli/FuzzIL/JSTyper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,37 @@ public struct JSTyper: Analyzer {
/// Attempts to infer the parameter types of the given subroutine definition.
/// If parameter types have been added for this function, they are returned, otherwise generic parameter types (i.e. .anything parameters) for the parameters specified in the operation are generated.
private func inferSubroutineParameterList(of op: BeginAnySubroutine, at index: Int) -> ParameterList {
return signatures[index] ?? ParameterList(numParameters: op.parameters.count, hasRestParam: op.parameters.hasRestParameter)
if let signature = signatures[index] {
return signature
} else {
var parameterList = ParameterList()
let patterns = op.parameters.patterns
let hasRestParam = op.parameters.hasRestParameter

for (i, pattern) in patterns.enumerated() {
let ilType = inferParameterType(from: pattern)
let parameter: Parameter
if hasRestParam && i == patterns.count - 1 {
parameter = .rest(ilType)
} else {
parameter = .plain(ilType)
}
parameterList.append(parameter)
}
return parameterList
}
}

private func inferParameterType(from pattern: ParameterPattern) -> ILType {
switch pattern {
case .identifier:
return .anything
case .array:
return .iterable
case .object:
// TODO be more precise here: Describe the object parameters structure
return .object()
saelo marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Set type to current state and save type change event
Expand Down
29 changes: 23 additions & 6 deletions Sources/Fuzzilli/FuzzIL/JsOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1047,21 +1047,38 @@ final class TestIn: JsOperation {

}

enum ParameterPattern {
saelo marked this conversation as resolved.
Show resolved Hide resolved
case identifier
case object(properties: [ObjectPatternProperty])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also be a [String: ParameterPattern] dictionary or would that make things more complicated? (asking because that "feels" a bit more natural without having thought about it deeply)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately switching to a dictionary based approach causes problem because it does not contain information about the order of the object property keys. The dictionary tells us which value belongs to which key but not in which order the keys appear in the object pattern. This is why I opted for an array.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah hm I see. But does the order matter? Shouldn't these behave in the same way:

function foo({a, b}) {}
function foo({b, a}) {}

Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically yes. The order of the parameters doesn't matter. However the mapping from parameter -> variable is lost this way (IIUC). I.e. the order of variables is fixed but the order of object properties is permutated.

We can do it anyway by using an array of tuples instead of a dictionary. This way we can retain the orderedness.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right yeah that's a good point. It also needs to be consistent across serialization and deserialization (see #449 (comment)) so I guess a list of tuples is the way to go here 👍

case array(elements: [ParameterPattern])
}

struct ObjectPatternProperty {
let key: String
let value: ParameterPattern
}

// The parameters of a FuzzIL subroutine.
public struct Parameters {
/// The total number of parameters.
private let numParameters: UInt32
/// The total number of variables after destructuring object- and array patterns.
/// Example: f({a, b}, [c, d]) has 4 variables, not 2.
private let numVariables: UInt32
/// Whether the last parameter is a rest parameter.
/// TODO make the rest parameter an actual parameter type.
let hasRestParameter: Bool
saelo marked this conversation as resolved.
Show resolved Hide resolved

/// The total number of parameters. This is equivalent to the number of inner outputs produced from the parameters.
var count: Int {
saelo marked this conversation as resolved.
Show resolved Hide resolved
return Int(numParameters)
return Int(numVariables)
}

init(count: Int, hasRestParameter: Bool = false) {
self.numParameters = UInt32(count)
var patterns: [ParameterPattern]
init(
count: Int,
hasRestParameter: Bool = false
) {
self.numVariables = UInt32(count)
self.hasRestParameter = hasRestParameter
self.patterns = []
}
}

Expand Down
32 changes: 25 additions & 7 deletions Sources/Fuzzilli/Lifting/JavaScriptLifter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1355,15 +1355,33 @@ public class JavaScriptLifter: Lifter {

private func liftParameters(_ parameters: Parameters, as variables: [String]) -> String {
saelo marked this conversation as resolved.
Show resolved Hide resolved
assert(parameters.count == variables.count)
var paramList = [String]()
for v in variables {
if parameters.hasRestParameter && v == variables.last {
paramList.append("..." + v)
} else {
paramList.append(v)
var variableIndex = 0
func liftPattern(_ pattern: ParameterPattern) -> String {
switch pattern {
case .identifier:
let variableName = variables[variableIndex]
variableIndex += 1
return variableName

case .object(let properties):
let liftedProperties = properties.map { property -> String in
let key = property.key
let value = liftPattern(property.value)
return "\(key): \(value)"
}
return "{ " + liftedProperties.joined(separator: ", ") + " }"

case .array(let elements):
let liftedElements = elements.map { element -> String in
return liftPattern(element)
}
return "[ " + liftedElements.joined(separator: ", ") + " ]"
}
}
return paramList.joined(separator: ", ")
let liftedParams = parameters.patterns.map { pattern in
return liftPattern(pattern)
}
return liftedParams.joined(separator: ", ")
}

private func liftFunctionDefinitionBegin(_ instr: Instruction, keyword FUNCTION: String, using w: inout JavaScriptWriter) {
Expand Down
Loading