Skip to content

Commit

Permalink
Use Handler in Purity checking (FuelLabs#3056)
Browse files Browse the repository at this point in the history
Based on FuelLabs#2988 and working towards
FuelLabs#2734, this PR uses the `Handler`
in purity checking, also enabling error recovery on the sway.

N.B. You'll want to review this with whitespace changes hidden.
  • Loading branch information
Centril authored Oct 18, 2022
1 parent dbd6b92 commit 335f4b0
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 195 deletions.
2 changes: 1 addition & 1 deletion sway-core/src/ir_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use sway_error::error::CompileError;
use sway_ir::Context;
use sway_types::span::Span;

pub(crate) use purity::PurityChecker;
pub(crate) use purity::{check_function_purity, PurityEnv};

use crate::language::ty;

Expand Down
231 changes: 110 additions & 121 deletions sway-core/src/ir_generation/purity.rs
Original file line number Diff line number Diff line change
@@ -1,140 +1,129 @@
use crate::{
error::*,
language::{promote_purity, Purity},
language::{
promote_purity,
Purity::{self, *},
},
metadata::{MetadataManager, StorageOperation},
};

use sway_error::error::CompileError;
use sway_error::warning::{CompileWarning, Warning};
use sway_error::{error::CompileError, handler::Handler};
use sway_ir::{Context, Function, Instruction};
use sway_types::span::Span;

use std::collections::HashMap;

#[derive(Default)]
pub(crate) struct PurityChecker {
pub(crate) struct PurityEnv {
memos: HashMap<Function, (bool, bool)>,

// Final results.
warnings: Vec<CompileWarning>,
errors: Vec<CompileError>,
}

impl PurityChecker {
/// Designed to be called for each entry point, _prior_ to inlining or other optimizations.
/// The checker will check this function and any that it calls.
///
/// Returns bools for whether it (reads, writes).
pub(crate) fn check_function(
&mut self,
context: &Context,
md_mgr: &mut MetadataManager,
function: &Function,
) -> (bool, bool) {
// Iterate for each instruction in the function and gather whether we have read and/or
// write storage operations:
// - via the storage IR instructions,
// - via ASM blocks with storage VM instructions or
// - via calls into functions with the above.
let (reads, writes) = function.instruction_iter(context).fold(
(false, false),
|(reads, writes), (_block, ins_value)| {
ins_value
.get_instruction(context)
.map(|instruction| {
match instruction {
Instruction::StateLoadQuadWord { .. }
| Instruction::StateLoadWord(_) => (true, writes),

Instruction::StateStoreQuadWord { .. }
| Instruction::StateStoreWord { .. } => (reads, true),

// Iterate for and check each instruction in the ASM block.
Instruction::AsmBlock(asm_block, _args) => {
asm_block.get_content(context).body.iter().fold(
(reads, writes),
|(reads, writes), asm_op| match asm_op.name.as_str() {
"srw" | "srwq" => (true, writes),
"sww" | "swwq" => (reads, true),
_ => (reads, writes),
},
)
}

// Recurse to find the called function purity. Use memoisation to
// avoid redoing work.
Instruction::Call(callee, _args) => {
let (called_fn_reads, called_fn_writes) =
self.memos.get(callee).copied().unwrap_or_else(|| {
let r_w = self.check_function(context, md_mgr, callee);
self.memos.insert(*callee, r_w);
r_w
});
(reads || called_fn_reads, writes || called_fn_writes)
}

_otherwise => (reads, writes),
/// Analyses purity annotations on functions.
///
/// Designed to be called for each entry point, _prior_ to inlining or other optimizations.
/// The checker will check this function and any that it calls.
///
/// Returns bools for whether it (reads, writes).
pub(crate) fn check_function_purity(
handler: &Handler,
env: &mut PurityEnv,
context: &Context,
md_mgr: &mut MetadataManager,
function: &Function,
) -> (bool, bool) {
// Iterate for each instruction in the function and gather whether we have read and/or
// write storage operations:
// - via the storage IR instructions,
// - via ASM blocks with storage VM instructions or
// - via calls into functions with the above.
let (reads, writes) = function.instruction_iter(context).fold(
(false, false),
|(reads, writes), (_block, ins_value)| {
ins_value
.get_instruction(context)
.map(|instruction| {
match instruction {
Instruction::StateLoadQuadWord { .. } | Instruction::StateLoadWord(_) => {
(true, writes)
}
})
.unwrap_or_else(|| (reads, writes))
},
);

let attributed_purity = md_mgr.md_to_storage_op(context, function.get_metadata(context));
let span = md_mgr
.md_to_span(context, function.get_metadata(context))
.unwrap_or_else(Span::dummy);

// Simple macros for each of the error types, which also grab `span`.
macro_rules! mk_err {
($op_str:literal, $existing_attrib:ident, $needed_attrib:ident) => {{
self.errors.push(CompileError::ImpureInPureContext {
storage_op: $op_str,
attrs: promote_purity(Purity::$existing_attrib, Purity::$needed_attrib)
.to_attribute_syntax(),
span,
});
}};
}
macro_rules! mk_warn {
($unneeded_attrib:ident) => {{
self.warnings.push(CompileWarning {
warning_content: Warning::DeadStorageDeclarationForFunction {
unneeded_attrib: Purity::$unneeded_attrib.to_attribute_syntax(),
},
span,
});
}};
}

match (attributed_purity, reads, writes) {
// Has no attributes but needs some.
(None, true, false) => mk_err!("read", Pure, Reads),
(None, false, true) => mk_err!("write", Pure, Writes),
(None, true, true) => mk_err!("read & write", Pure, ReadsWrites),

// Or the attribute must match the behaviour.
(Some(StorageOperation::Reads), _, true) => mk_err!("write", Reads, Writes),
(Some(StorageOperation::Writes), true, _) => mk_err!("read", Writes, Reads),

// Or we have unneeded attributes.
(Some(StorageOperation::ReadsWrites), false, true) => mk_warn!(Reads),
(Some(StorageOperation::ReadsWrites), true, false) => mk_warn!(Writes),
(Some(StorageOperation::Reads), false, false) => mk_warn!(Reads),
(Some(StorageOperation::Writes), false, false) => mk_warn!(Writes),

// (Pure, false, false) is OK, as is (ReadsWrites, true, true).
_otherwise => (),
};
Instruction::StateStoreQuadWord { .. }
| Instruction::StateStoreWord { .. } => (reads, true),

// Iterate for and check each instruction in the ASM block.
Instruction::AsmBlock(asm_block, _args) => {
asm_block.get_content(context).body.iter().fold(
(reads, writes),
|(reads, writes), asm_op| match asm_op.name.as_str() {
"srw" | "srwq" => (true, writes),
"sww" | "swwq" => (reads, true),
_ => (reads, writes),
},
)
}

(reads, writes)
}
// Recurse to find the called function purity. Use memoisation to
// avoid redoing work.
Instruction::Call(callee, _args) => {
let (called_fn_reads, called_fn_writes) =
env.memos.get(callee).copied().unwrap_or_else(|| {
let r_w = check_function_purity(
handler, env, context, md_mgr, callee,
);
env.memos.insert(*callee, r_w);
r_w
});
(reads || called_fn_reads, writes || called_fn_writes)
}

pub(crate) fn results(self) -> CompileResult<()> {
if self.errors.is_empty() {
ok((), self.warnings, self.errors)
} else {
err(self.warnings, self.errors)
}
}
_otherwise => (reads, writes),
}
})
.unwrap_or_else(|| (reads, writes))
},
);

let attributed_purity = md_mgr.md_to_storage_op(context, function.get_metadata(context));
let span = md_mgr
.md_to_span(context, function.get_metadata(context))
.unwrap_or_else(Span::dummy);

// Simple closures for each of the error types.
let error = |span, storage_op, existing, needed| {
handler.emit_err(CompileError::ImpureInPureContext {
storage_op,
attrs: promote_purity(existing, needed).to_attribute_syntax(),
span,
});
};
let warn = |span, purity: Purity| {
handler.emit_warn(CompileWarning {
warning_content: Warning::DeadStorageDeclarationForFunction {
unneeded_attrib: purity.to_attribute_syntax(),
},
span,
});
};

match (attributed_purity, reads, writes) {
// Has no attributes but needs some.
(None, true, false) => error(span, "read", Pure, Reads),
(None, false, true) => error(span, "write", Pure, Writes),
(None, true, true) => error(span, "read & write", Pure, ReadsWrites),

// Or the attribute must match the behaviour.
(Some(StorageOperation::Reads), _, true) => error(span, "write", Reads, Writes),
(Some(StorageOperation::Writes), true, _) => error(span, "read", Writes, Reads),

// Or we have unneeded attributes.
(Some(StorageOperation::ReadsWrites), false, true) => warn(span, Reads),
(Some(StorageOperation::ReadsWrites), true, false) => warn(span, Writes),
(Some(StorageOperation::Reads), false, false) => warn(span, Reads),
(Some(StorageOperation::Writes), false, false) => warn(span, Writes),

// (Pure, false, false) is OK, as is (ReadsWrites, true, true).
_ => (),
};

(reads, writes)
}
Loading

0 comments on commit 335f4b0

Please sign in to comment.