///|
pub let replay_schema_version : Int = 1
///|
pub(all) struct ReplayRecord {
schema_version : Int
target_name : String
seed : Int64
input : Bytes
feedback_signature : FeedbackSignature
failure_fingerprint : FailureFingerprint?
result_digest : String?
generation_budget : GenerationBudget
} derive(Debug, Eq)
///|
pub fn ReplayRecord::new(
target_name~ : String,
seed~ : Int64,
input~ : Bytes,
observation~ : TargetObservation,
generation_budget? : GenerationBudget = GenerationBudget::default(),
) -> ReplayRecord {
{
schema_version: replay_schema_version,
target_name,
seed,
input,
feedback_signature: observation.signature(),
failure_fingerprint: observation.failure_fingerprint(),
result_digest: observation.result_digest,
generation_budget,
}
}
///|
pub(all) enum ReplayError {
InvalidJson
MissingField(String)
InvalidField(String)
UnsupportedReplayVersion(Int)
ArtifactLimitExceeded(Int)
InputLimitExceeded(Int)
InvalidHexInput(Int)
TargetNameMismatch(String, String)
} derive(Debug, Eq)
///|
pub(all) struct ReplayOutcome {
observation : TargetObservation
feedback_matches : Bool
failure_matches : Bool
result_matches : Bool
} derive(Debug, Eq)
///|
pub fn ReplayOutcome::matches(self : ReplayOutcome) -> Bool {
self.feedback_matches && self.failure_matches && self.result_matches
}
///|
fn replay_json_quote(value : String) -> String {
Json::string(value).stringify()
}
///|
fn replay_hex(input : Bytes) -> String {
let builder = StringBuilder(size_hint=input.length() * 2)
for byte in input {
let value = byte.to_int()
builder.write_char(hex_digit(value / 16))
builder.write_char(hex_digit(value % 16))
}
builder.to_string()
}
///|
fn optional_json_string(value : String?) -> String {
match value {
None => "null"
Some(text) => replay_json_quote(text)
}
}
///|
pub fn ReplayRecord::encode_json(self : ReplayRecord) -> String {
let failure = match self.failure_fingerprint {
None => None
Some(value) => Some(value.value())
}
"{" +
"\"schema_version\":" +
self.schema_version.to_string() +
",\"target_name\":" +
replay_json_quote(self.target_name) +
",\"seed\":" +
replay_json_quote(self.seed.to_string()) +
",\"input_hex\":" +
replay_json_quote(replay_hex(self.input)) +
",\"feedback_signature\":" +
replay_json_quote(self.feedback_signature.value()) +
",\"failure_fingerprint\":" +
optional_json_string(failure) +
",\"result_digest\":" +
optional_json_string(self.result_digest) +
",\"generation_budget\":{" +
"\"max_depth\":" +
self.generation_budget.max_depth.to_string() +
",\"max_nodes\":" +
self.generation_budget.max_nodes.to_string() +
",\"max_bytes\":" +
self.generation_budget.max_bytes.to_string() +
",\"max_repeat\":" +
self.generation_budget.max_repeat.to_string() +
"}}"
}
///|
fn replay_object(
value : Json,
name : String,
) -> Result[Map[String, Json], ReplayError] {
match value {
Object(fields) => Ok(fields)
_ => Err(InvalidField(name + " must be an object"))
}
}
///|
fn replay_required(
fields : Map[String, Json],
name : String,
) -> Result[Json, ReplayError] {
match fields.get(name) {
None => Err(MissingField(name))
Some(value) => Ok(value)
}
}
///|
fn replay_int(
fields : Map[String, Json],
name : String,
) -> Result[Int, ReplayError] {
let value = match replay_required(fields, name) {
Err(error) => return Err(error)
Ok(value) => value
}
let decoded : Int = @json.from_json(value) catch {
_ => return Err(InvalidField(name + " must be an integer"))
}
Ok(decoded)
}
///|
fn replay_string(
fields : Map[String, Json],
name : String,
) -> Result[String, ReplayError] {
match replay_required(fields, name) {
Err(error) => Err(error)
Ok(String(value)) => Ok(value)
Ok(_) => Err(InvalidField(name + " must be a string"))
}
}
///|
fn replay_optional_string(
fields : Map[String, Json],
name : String,
) -> Result[String?, ReplayError] {
match replay_required(fields, name) {
Err(error) => Err(error)
Ok(Null) => Ok(None)
Ok(String(value)) => Ok(Some(value))
Ok(_) => Err(InvalidField(name + " must be a string or null"))
}
}
///|
fn hex_value(byte : Byte) -> Int? {
let value = byte.to_int()
if value >= '0'.to_int() && value <= '9'.to_int() {
Some(value - '0'.to_int())
} else if value >= 'a'.to_int() && value <= 'f'.to_int() {
Some(value - 'a'.to_int() + 10)
} else if value >= 'A'.to_int() && value <= 'F'.to_int() {
Some(value - 'A'.to_int() + 10)
} else {
None
}
}
///|
fn decode_replay_hex(
value : String,
maximum : Int,
) -> Result[Bytes, ReplayError] {
let encoded = @utf8.encode(value)
if encoded.length() % 2 != 0 {
return Err(InvalidHexInput(encoded.length()))
}
if encoded.length() / 2 > maximum {
return Err(InputLimitExceeded(maximum))
}
let output : Array[Byte] = []
let mut index = 0
while index < encoded.length() {
let high = match hex_value(encoded[index]) {
None => return Err(InvalidHexInput(index))
Some(value) => value
}
let low = match hex_value(encoded[index + 1]) {
None => return Err(InvalidHexInput(index + 1))
Some(value) => value
}
output.push((high * 16 + low).to_byte())
index += 2
}
Ok(Bytes::from_array(output))
}
///|
fn parse_seed(value : String) -> Result[Int64, ReplayError] {
let parsed : Int64 = @string.parse_int64(value) catch {
_ => return Err(InvalidField("seed must be a signed 64-bit integer"))
}
Ok(parsed)
}
///|
pub fn ReplayRecord::decode_json(
text : String,
max_input_bytes? : Int = 1024 * 1024,
max_artifact_bytes? : Int = 4 * 1024 * 1024,
) -> Result[ReplayRecord, ReplayError] {
if @utf8.encode(text).length() > max_artifact_bytes.max(0) {
return Err(ArtifactLimitExceeded(max_artifact_bytes.max(0)))
}
let parsed = @json.parse(text) catch { _ => return Err(InvalidJson) }
let fields = match replay_object(parsed, "replay root") {
Err(error) => return Err(error)
Ok(value) => value
}
let version = match replay_int(fields, "schema_version") {
Err(error) => return Err(error)
Ok(value) => value
}
if version != replay_schema_version {
return Err(UnsupportedReplayVersion(version))
}
let target_name = match replay_string(fields, "target_name") {
Err(error) => return Err(error)
Ok(value) => value
}
let seed_text = match replay_string(fields, "seed") {
Err(error) => return Err(error)
Ok(value) => value
}
let seed = match parse_seed(seed_text) {
Err(error) => return Err(error)
Ok(value) => value
}
let input_hex = match replay_string(fields, "input_hex") {
Err(error) => return Err(error)
Ok(value) => value
}
let input = match decode_replay_hex(input_hex, max_input_bytes.max(0)) {
Err(error) => return Err(error)
Ok(value) => value
}
let feedback = match replay_string(fields, "feedback_signature") {
Err(error) => return Err(error)
Ok(value) => FeedbackSignature(value)
}
let failure = match replay_optional_string(fields, "failure_fingerprint") {
Err(error) => return Err(error)
Ok(None) => None
Ok(Some(value)) => Some(FailureFingerprint(value))
}
let result_digest = match replay_optional_string(fields, "result_digest") {
Err(error) => return Err(error)
Ok(value) => value
}
let budget_json = match replay_required(fields, "generation_budget") {
Err(error) => return Err(error)
Ok(value) => value
}
let budget_fields = match replay_object(budget_json, "generation_budget") {
Err(error) => return Err(error)
Ok(value) => value
}
let max_depth = match replay_int(budget_fields, "max_depth") {
Err(error) => return Err(error)
Ok(value) => value
}
let max_nodes = match replay_int(budget_fields, "max_nodes") {
Err(error) => return Err(error)
Ok(value) => value
}
let max_bytes = match replay_int(budget_fields, "max_bytes") {
Err(error) => return Err(error)
Ok(value) => value
}
let max_repeat = match replay_int(budget_fields, "max_repeat") {
Err(error) => return Err(error)
Ok(value) => value
}
let generation_budget = { max_depth, max_nodes, max_bytes, max_repeat, }
match generation_budget.validate() {
Err(_) => return Err(InvalidField("generation_budget is invalid"))
Ok(_) => ()
}
Ok({
schema_version: version,
target_name,
seed,
input,
feedback_signature: feedback,
failure_fingerprint: failure,
result_digest,
generation_budget,
})
}
///|
pub fn Target::replay(
self : Target,
record : ReplayRecord,
) -> Result[ReplayOutcome, ReplayError] {
if self.name() != record.target_name {
return Err(TargetNameMismatch(record.target_name, self.name()))
}
let observation = self.run(record.input)
Ok({
feedback_matches: observation.signature() == record.feedback_signature,
failure_matches: observation.failure_fingerprint() ==
record.failure_fingerprint,
result_matches: observation.result_digest == record.result_digest,
observation,
})
}