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

Add fuzzing to the CI #246

Merged
merged 11 commits into from
Nov 11, 2022
15 changes: 15 additions & 0 deletions .github/workflows/compiler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,18 @@ jobs:
with:
command: clippy
args: --manifest-path compiler/Cargo.toml -- -D warnings

# fuzzing:
# name: Fuzzing
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: ${{ env.RUST_VERSION }}
# override: true
# - uses: actions-rs/cargo@v1
# with:
# command: run
# args: --manifest-path compiler/Cargo.toml -- fuzz packages/Benchmark.candy
3 changes: 3 additions & 0 deletions compiler/src/fuzzer/fuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ impl Fuzzer {
pub fn status(&self) -> &Status {
self.status.as_ref().unwrap()
}
pub fn into_status(self) -> Status {
self.status.unwrap()
}

pub fn run<U: UseProvider, E: ExecutionController>(
&mut self,
Expand Down
52 changes: 41 additions & 11 deletions compiler/src/fuzzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ use crate::{
module::Module,
vm::{
context::{DbUseProvider, RunForever, RunLimitedNumberOfInstructions},
Closure, Heap, Pointer, Vm,
tracer::full::FullTracer,
Closure, Heap, Packet, Pointer, Vm,
},
};
use itertools::Itertools;
use tracing::{error, info};

pub async fn fuzz(db: &Database, module: Module) {
pub async fn fuzz(db: &Database, module: Module) -> Vec<FailingFuzzCase> {
let (fuzzables_heap, fuzzables): (Heap, Vec<(Id, Pointer)>) = {
let mut tracer = FuzzablesFinder::default();
let mut vm = Vm::new();
Expand All @@ -32,30 +33,59 @@ pub async fn fuzz(db: &Database, module: Module) {
fuzzables.len()
);

let mut failing_cases = vec![];

for (id, closure) in fuzzables {
info!("Fuzzing {id}.");
let mut fuzzer = Fuzzer::new(&fuzzables_heap, closure, id.clone());
fuzzer.run(
&mut DbUseProvider { db },
&mut RunLimitedNumberOfInstructions::new(1000),
);
match fuzzer.status() {
match fuzzer.into_status() {
Status::StillFuzzing { .. } => {}
Status::PanickedForArguments {
arguments,
reason,
tracer,
} => {
error!("The fuzzer discovered an input that crashes {id}:");
error!(
"Calling `{id} {}` doesn't work because {reason}.",
arguments.iter().map(|arg| format!("{arg:?}")).join(" "),
);
error!(
"This is the stack trace:\n{}",
tracer.format_panic_stack_trace_to_root_fiber(db)
);
let case = FailingFuzzCase {
closure: id,
arguments,
reason,
tracer,
};
case.dump(db);
failing_cases.push(case);
}
}
}

failing_cases
}

pub struct FailingFuzzCase {
closure: Id,
arguments: Vec<Packet>,
reason: String,
tracer: FullTracer,
}

impl FailingFuzzCase {
pub fn dump(&self, db: &Database) {
error!(
"Calling `{} {}` doesn't work because {}.",
self.closure,
self.arguments
.iter()
.map(|arg| format!("{arg:?}"))
.join(" "),
self.reason,
);
error!(
"This is the stack trace:\n{}",
self.tracer.format_panic_stack_trace_to_root_fiber(db)
);
}
}
55 changes: 41 additions & 14 deletions compiler/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct CandyFuzzOptions {
}

#[tokio::main]
async fn main() {
async fn main() -> ProgramResult {
init_logger();
match CandyOptions::from_args() {
CandyOptions::Build(options) => build(options),
Expand All @@ -97,15 +97,25 @@ async fn main() {
}
}

fn build(options: CandyBuildOptions) {
type ProgramResult = Result<(), Exit>;
#[derive(Debug)]
enum Exit {
FileNotFound,
FuzzingFoundFailingCases,
CodePanicked,
}

fn build(options: CandyBuildOptions) -> ProgramResult {
let module = Module::from_package_root_and_file(
current_dir().unwrap(),
options.file.clone(),
ModuleKind::Code,
);
raw_build(module.clone(), options.debug);
let result = raw_build(module.clone(), options.debug);

if options.watch {
if !options.watch {
result.ok_or(Exit::FileNotFound).map(|_| ())
} else {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
watcher
Expand Down Expand Up @@ -192,7 +202,7 @@ fn raw_build(module: Module, debug: bool) -> Option<Arc<Lir>> {
Some(lir)
}

fn run(options: CandyRunOptions) {
fn run(options: CandyRunOptions) -> ProgramResult {
let module = Module::from_package_root_and_file(
current_dir().unwrap(),
options.file.clone(),
Expand All @@ -201,8 +211,8 @@ fn run(options: CandyRunOptions) {
let db = Database::default();

if raw_build(module.clone(), false).is_none() {
warn!("Build failed.");
return;
warn!("File not found.");
return Err(Exit::FileNotFound);
};
// TODO: Optimize the code before running.

Expand Down Expand Up @@ -251,7 +261,7 @@ fn run(options: CandyRunOptions) {
"This is the stack trace:\n{}",
tracer.format_panic_stack_trace_to_root_fiber(&db)
);
return;
return Err(Exit::CodePanicked);
}
};

Expand All @@ -260,7 +270,7 @@ fn run(options: CandyRunOptions) {
Some(main) => main,
None => {
error!("The module doesn't contain a main function.");
return;
return Err(Exit::CodePanicked);
}
};

Expand Down Expand Up @@ -302,6 +312,7 @@ fn run(options: CandyRunOptions) {
.for_fiber(FiberId::root())
.call_ended(return_value.address, &return_value.heap);
debug!("The main function returned: {return_value:?}");
Ok(())
}
ExecutionResult::Panicked {
reason,
Expand All @@ -317,6 +328,7 @@ fn run(options: CandyRunOptions) {
"This is the stack trace:\n{}",
tracer.format_panic_stack_trace_to_root_fiber(&db)
);
Err(Exit::CodePanicked)
}
}
}
Expand Down Expand Up @@ -346,29 +358,44 @@ impl StdoutService {
}
}

async fn fuzz(options: CandyFuzzOptions) {
async fn fuzz(options: CandyFuzzOptions) -> ProgramResult {
let module = Module::from_package_root_and_file(
current_dir().unwrap(),
options.file.clone(),
ModuleKind::Code,
);

if raw_build(module.clone(), false).is_none() {
warn!("Build failed.");
return;
warn!("File not found.");
return Err(Exit::FileNotFound);
}

debug!("Fuzzing `{module}`.");
let db = Database::default();
fuzzer::fuzz(&db, module).await;
let failing_cases = fuzzer::fuzz(&db, module).await;

if failing_cases.is_empty() {
info!("All found fuzzable closures seem fine.");
Ok(())
} else {
error!("");
error!("Finished fuzzing.");
error!("These are the failing cases:");
for case in failing_cases {
error!("");
case.dump(&db);
}
Err(Exit::FuzzingFoundFailingCases)
}
}

async fn lsp() {
async fn lsp() -> ProgramResult {
info!("Starting language server…");
let (service, socket) = LspService::new(CandyLanguageServer::from_client);
Server::new(tokio::io::stdin(), tokio::io::stdout(), socket)
.serve(service)
.await;
Ok(())
}

fn init_logger() {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub use self::{
fiber::{ExecutionResult, Fiber},
heap::{Closure, Heap, Object, Pointer, Struct},
ids::{ChannelId, FiberId, OperationId},
tracer::{full::FullTracer, Tracer},
};
use self::{
channel::{Channel, Completer, Performer},
Expand All @@ -21,7 +22,6 @@ use self::{
},
heap::SendPort,
ids::{CountableId, IdGenerator},
tracer::Tracer,
};
use crate::compiler::hir::Id;
use itertools::Itertools;
Expand Down
23 changes: 9 additions & 14 deletions packages/Benchmark.candy
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@

core = use "..Core"

fibRec fibRec n =
fibRec = { fibRec n ->
core.ifElse (core.int.isLessThan n 2) { n } {
core.int.add
fibRec fibRec (core.int.subtract n 1)
fibRec fibRec (core.int.subtract n 2)
}
fib n = fibRec fibRec n
}
fib n =
needs (core.int.is n)
fibRec fibRec n
twentyOne := fib 8

main := { environment ->
print message = { core.channel.send environment.stdout message }
print "Hello, world!"

core.parallel { nursery ->
print (core.await (core.async nursery {
print "Hello from fiber!"
"Hello, async await!"
}))
print message =
needs (core.text.is message)
core.channel.send environment.stdout message

core.async nursery { print "Kiwi" }
core.async nursery { print "Banana" }
print "Hi"
}
print "Hello, world!"
}
9 changes: 7 additions & 2 deletions packages/Core/channel.candy
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
bool = use "..bool"
equals = (use "..equality").equals
if = (use "..conditionals").if
isInt = (use "..int").is
int = use "..int"
isType = (use "..type").is

isSendPort value := isType value SendPort
isReceivePort value := isType value ReceivePort

create capacity :=
needs (isInt capacity) "`capacity` is not an integer. Channels need a capacity because otherwise, there would be no backpressure among fibers, and memory leaks would go unnoticed."
needs (int.is capacity) "`capacity` is not an integer. Channels need a capacity because otherwise, there would be no backpressure among fibers, and memory leaks would go unnoticed."
needs (int.isNonNegative capacity)
needs (int.fitsInRustU32 capacity)
# Technically, it needs to fit in a usize, the natural word length on systems.
# Typically, this is 64-bits, so we are conservative here, although there
# also exist exotic/old systems with smaller word sizes of 16 bits.
✨.channelCreate capacity

send port packet :=
Expand Down
7 changes: 7 additions & 0 deletions packages/Core/int.candy
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ absolute value :=
needs (is value)
conditionals.ifElse (isNegative value) (negate value) value

fitsInRustU32 value :=
needs (is value)
needs (isNonNegative value) "you gave `fitsInRustU128` a signed int"

rustU32Max = 4294967295
# https://doc.rust-lang.org/std/primitive.u32.html#associatedconstant.MAX
isLessThan value rustU32Max
fitsInRustU128 value =
needs (is value)
needs (isNonNegative value)
Expand Down