Better error hanlding when parsing, added medical system
fixed grammar
This commit is contained in:
@ -8,10 +8,10 @@ lalrpop = "0.22"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = { version = "*" }
|
rand = { version = "*" }
|
||||||
|
colored = { version = "*" }
|
||||||
regex = { version = "1", features = ["unicode-bool"] }
|
regex = { version = "1", features = ["unicode-bool"] }
|
||||||
lalrpop-util = { version = "*", features = ["lexer", "unicode"] }
|
lalrpop-util = { version = "*", features = ["lexer", "unicode"] }
|
||||||
petgraph = { version = "*", features = ["serde-1"] }
|
petgraph = { version = "*", features = ["serde-1"] }
|
||||||
# TODO remove git and use crates.io version when updated
|
|
||||||
petgraph-graphml = { version = "5" }
|
petgraph-graphml = { version = "5" }
|
||||||
serde = { version = "1", features = ["derive", "rc"] }
|
serde = { version = "1", features = ["derive", "rc"] }
|
||||||
serde_cbor_2 = { version = "*" }
|
serde_cbor_2 = { version = "*" }
|
||||||
|
|||||||
@ -2,7 +2,7 @@ fn main() {
|
|||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
use reactionsystems::rsprocess::presets;
|
use reactionsystems::rsprocess::presets;
|
||||||
match presets::run("testing/first.system".into()) {
|
match presets::run("testing/medical.system".into()) {
|
||||||
Ok(()) => {},
|
Ok(()) => {},
|
||||||
Err(e) => {println!("{e}")}
|
Err(e) => {println!("{e}")}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,8 +112,8 @@ impl RSassert<EdgeRelablerInput> {
|
|||||||
AssertionTypes::Context =>
|
AssertionTypes::Context =>
|
||||||
Ok(()),
|
Ok(()),
|
||||||
AssertionTypes::NoType =>
|
AssertionTypes::NoType =>
|
||||||
Err(format!("No return type, at least one return statement \
|
Err("No return type, at least one return statement \
|
||||||
required.")),
|
required.".into()),
|
||||||
AssertionTypes::RangeInteger |
|
AssertionTypes::RangeInteger |
|
||||||
AssertionTypes::RangeSet |
|
AssertionTypes::RangeSet |
|
||||||
AssertionTypes::RangeNeighbours =>
|
AssertionTypes::RangeNeighbours =>
|
||||||
@ -228,8 +228,8 @@ impl RSassert<NodeRelablerInput> {
|
|||||||
AssertionTypes::Context =>
|
AssertionTypes::Context =>
|
||||||
Ok(()),
|
Ok(()),
|
||||||
AssertionTypes::NoType =>
|
AssertionTypes::NoType =>
|
||||||
Err(format!("No return type, at least one return statement \
|
Err("No return type, at least one return statement \
|
||||||
required.")),
|
required.".into()),
|
||||||
AssertionTypes::RangeInteger |
|
AssertionTypes::RangeInteger |
|
||||||
AssertionTypes::RangeSet |
|
AssertionTypes::RangeSet |
|
||||||
AssertionTypes::RangeNeighbours =>
|
AssertionTypes::RangeNeighbours =>
|
||||||
|
|||||||
@ -44,15 +44,16 @@ match {
|
|||||||
"Products", "MaskProducts", "UncommonProducts", "UncommonMaskProducts",
|
"Products", "MaskProducts", "UncommonProducts", "UncommonMaskProducts",
|
||||||
"Union", "MaskUnion", "UncommonUnion", "UncommonMaskUnion",
|
"Union", "MaskUnion", "UncommonUnion", "UncommonMaskUnion",
|
||||||
"Difference", "MaskDifference",
|
"Difference", "MaskDifference",
|
||||||
"UncommonDifference", "UncommonMaskDifference",
|
"UncommonDifference", "UncommonMaskDifference",
|
||||||
"EntitiesDeleted", "MaskEntitiesDeleted",
|
"EntitiesDeleted", "MaskEntitiesDeleted",
|
||||||
"UncommonEntitiesDeleted", "UncommonMaskEntitiesDeleted",
|
"UncommonEntitiesDeleted", "UncommonMaskEntitiesDeleted",
|
||||||
"EntitiesAdded", "MaskEntitiesAdded",
|
"EntitiesAdded", "MaskEntitiesAdded",
|
||||||
"UncommonEntitiesAdded", "UncommonMaskEntitiesAdded",
|
"UncommonEntitiesAdded", "UncommonMaskEntitiesAdded",
|
||||||
"label", "edge", "if", "then", "else", "let", "=", "return", "for", "in",
|
"label", "edge", "if", "then", "else", "let", "=", "return", "for", "in",
|
||||||
"not", "rand", ".empty", ".length", ".tostr", ".source", ".target",
|
"not", "rand", "empty", "length", "tostr", "source", "target",
|
||||||
"&&", "||", "^^", "<=", ">=", "==", "!=", "+", "*", "/", "%",
|
"&&", "||", "^^", "<=", ">=", "==", "!=", "+", "*", "/", "%",
|
||||||
"::", "substr", "min", "max", "commonsubstr",
|
"::",
|
||||||
|
"substr", "min", "max", "commonsubstr",
|
||||||
"SystemEntities", "SystemContext",
|
"SystemEntities", "SystemContext",
|
||||||
"AvailableEntities", "AllReactants", "AllInhibitors",
|
"AvailableEntities", "AllReactants", "AllInhibitors",
|
||||||
"relabel",
|
"relabel",
|
||||||
@ -67,16 +68,96 @@ match {
|
|||||||
_
|
_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// matches words (letter followed by numbers, letters or _)
|
// matches words (letter followed by numbers, letters or _)
|
||||||
Literal: String = {
|
Literal: String = {
|
||||||
WORD => <>.to_string()
|
WORD => <>.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
LiteralProcess: String = {
|
||||||
|
Literal => <>,
|
||||||
|
"true" => <>.into(),
|
||||||
|
"false" => <>.into(),
|
||||||
|
"Environment" => <>.into(),
|
||||||
|
"Initial" => <>.into(),
|
||||||
|
"Context" => <>.into(),
|
||||||
|
"Reactions" => <>.into(),
|
||||||
|
"Weights" => <>.into(),
|
||||||
|
"Sets" => <>.into(),
|
||||||
|
"Print" => <>.into(),
|
||||||
|
"Save" => <>.into(),
|
||||||
|
"Dot" => <>.into(),
|
||||||
|
"GraphML" => <>.into(),
|
||||||
|
"Serialize" => <>.into(),
|
||||||
|
"Stats" => <>.into(),
|
||||||
|
"Target" => <>.into(),
|
||||||
|
"Run" => <>.into(),
|
||||||
|
"Loop" => <>.into(),
|
||||||
|
"Frequency" => <>.into(),
|
||||||
|
"LimitFrequency" => <>.into(),
|
||||||
|
"FastFrequency" => <>.into(),
|
||||||
|
"Digraph" => <>.into(),
|
||||||
|
"Bisimilarity" => <>.into(),
|
||||||
|
"Deserialize" => <>.into(),
|
||||||
|
"Hide" => <>.into(),
|
||||||
|
"Entities" => <>.into(),
|
||||||
|
"MaskEntities" => <>.into(),
|
||||||
|
"UncommonEntities" => <>.into(),
|
||||||
|
"UncommonMaskEntities" => <>.into(),
|
||||||
|
"MaskContext" => <>.into(),
|
||||||
|
"UncommonContext" => <>.into(),
|
||||||
|
"UncommonMaskContext" => <>.into(),
|
||||||
|
"Products" => <>.into(),
|
||||||
|
"MaskProducts" => <>.into(),
|
||||||
|
"UncommonProducts" => <>.into(),
|
||||||
|
"UncommonMaskProducts" => <>.into(),
|
||||||
|
"Union" => <>.into(),
|
||||||
|
"MaskUnion" => <>.into(),
|
||||||
|
"UncommonUnion" => <>.into(),
|
||||||
|
"UncommonMaskUnion" => <>.into(),
|
||||||
|
"Difference" => <>.into(),
|
||||||
|
"MaskDifference" => <>.into(),
|
||||||
|
"UncommonDifference" => <>.into(),
|
||||||
|
"UncommonMaskDifference" => <>.into(),
|
||||||
|
"EntitiesDeleted" => <>.into(),
|
||||||
|
"MaskEntitiesDeleted" => <>.into(),
|
||||||
|
"UncommonEntitiesDeleted" => <>.into(),
|
||||||
|
"UncommonMaskEntitiesDeleted" => <>.into(),
|
||||||
|
"EntitiesAdded" => <>.into(),
|
||||||
|
"MaskEntitiesAdded" => <>.into(),
|
||||||
|
"UncommonEntitiesAdded" => <>.into(),
|
||||||
|
"UncommonMaskEntitiesAdded" => <>.into(),
|
||||||
|
"label" => <>.into(),
|
||||||
|
"edge" => <>.into(),
|
||||||
|
"if" => <>.into(),
|
||||||
|
"then" => <>.into(),
|
||||||
|
"else" => <>.into(),
|
||||||
|
"let" => <>.into(),
|
||||||
|
"return" => <>.into(),
|
||||||
|
"for" => <>.into(),
|
||||||
|
"in" => <>.into(),
|
||||||
|
"not" => <>.into(),
|
||||||
|
"rand" => <>.into(),
|
||||||
|
"empty" => <>.into(),
|
||||||
|
"length" => <>.into(),
|
||||||
|
"tostr" => <>.into(),
|
||||||
|
"source" => <>.into(),
|
||||||
|
"target" => <>.into(),
|
||||||
|
"substr" => <>.into(),
|
||||||
|
"min" => <>.into(),
|
||||||
|
"max" => <>.into(),
|
||||||
|
"commonsubstr" => <>.into(),
|
||||||
|
"SystemEntities" => <>.into(),
|
||||||
|
"SystemContext" => <>.into(),
|
||||||
|
"AvailableEntities" => <>.into(),
|
||||||
|
"AllReactants" => <>.into(),
|
||||||
|
"AllInhibitors" => <>.into(),
|
||||||
|
"relabel" => <>.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// all numbers are i64
|
// all numbers are i64
|
||||||
Num: i64 = {
|
Num: i64 = {
|
||||||
NUMBER =>? i64::from_str(<>)
|
NUMBER =>? i64::from_str(<>)
|
||||||
.map_err(|_| ParseError::User { error: "Number is too big" })
|
.map_err(|_| ParseError::User { error: "Number is too big" })
|
||||||
};
|
};
|
||||||
|
|
||||||
Path: String = {
|
Path: String = {
|
||||||
@ -124,7 +205,10 @@ pub Reactions: Vec<RSreaction> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Reaction: RSreaction = {
|
Reaction: RSreaction = {
|
||||||
|
#[precedence(level="1")]
|
||||||
"[" <r: Set> "," <i: Set> "," <p: Set> "]" => RSreaction::from(r, i, p),
|
"[" <r: Set> "," <i: Set> "," <p: Set> "]" => RSreaction::from(r, i, p),
|
||||||
|
|
||||||
|
#[precedence(level="0")]
|
||||||
"[" "r:" <r: Set> "," "i:" <i: Set> "," "p:" <p: Set> "]" =>
|
"[" "r:" <r: Set> "," "i:" <i: Set> "," "p:" <p: Set> "]" =>
|
||||||
RSreaction::from(r, i, p),
|
RSreaction::from(r, i, p),
|
||||||
}
|
}
|
||||||
@ -145,18 +229,26 @@ Boxed_CTX_process: Rc<RSprocess> = {
|
|||||||
|
|
||||||
CTX_process: RSprocess = {
|
CTX_process: RSprocess = {
|
||||||
"nill" => RSprocess::Nill,
|
"nill" => RSprocess::Nill,
|
||||||
|
|
||||||
<c: Set_of_entities> "." <k: CTX_process> =>
|
<c: Set_of_entities> "." <k: CTX_process> =>
|
||||||
RSprocess::EntitySet{ entities: c, next_process: Rc::new(k) },
|
RSprocess::EntitySet{ entities: c, next_process: Rc::new(k) },
|
||||||
|
|
||||||
"(" <k: CTX_process> ")" => k,
|
"(" <k: CTX_process> ")" => k,
|
||||||
|
|
||||||
"(" <k: Separated<CTX_process, "+">> ")" =>
|
"(" <k: Separated<CTX_process, "+">> ")" =>
|
||||||
RSprocess::Summation{
|
RSprocess::Summation{
|
||||||
children: k.into_iter().map(Rc::new).collect::<Vec<_>>()
|
children: k.into_iter().map(Rc::new).collect::<Vec<_>>()
|
||||||
},
|
},
|
||||||
"<" <n: Num> <k1: CTX_process> ">" "." <k: CTX_process> =>
|
|
||||||
|
"?" <r: Reaction> "?" "." <k: CTX_process> =>
|
||||||
|
RSprocess::Guarded{ reaction: r, next_process: Rc::new(k) },
|
||||||
|
|
||||||
|
"<" <n: Num> "," <k1: CTX_process> ">" "." <k: CTX_process> =>
|
||||||
RSprocess::WaitEntity{ repeat: n,
|
RSprocess::WaitEntity{ repeat: n,
|
||||||
repeated_process: Rc::new(k1),
|
repeated_process: Rc::new(k1),
|
||||||
next_process: Rc::new(k) },
|
next_process: Rc::new(k) },
|
||||||
<identifier: Literal> =>
|
|
||||||
|
<identifier: LiteralProcess> =>
|
||||||
RSprocess::RecursiveIdentifier{
|
RSprocess::RecursiveIdentifier{
|
||||||
identifier: translator.encode(identifier)
|
identifier: translator.encode(identifier)
|
||||||
}
|
}
|
||||||
@ -172,7 +264,7 @@ pub Environment: Box<RSenvironment> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Env_term: (IdType, RSprocess) = {
|
Env_term: (IdType, RSprocess) = {
|
||||||
<identifier: Literal> "=" <k: CTX_process> =>
|
<identifier: LiteralProcess> "=" <k: CTX_process> =>
|
||||||
(translator.encode(identifier), k)
|
(translator.encode(identifier), k)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -192,61 +284,78 @@ AssertTree: rsassert::Tree = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AssertTree2: rsassert::Tree = {
|
AssertTree2: rsassert::Tree = {
|
||||||
|
#[precedence(level="1")]
|
||||||
"if" <e: AssertExpression>
|
"if" <e: AssertExpression>
|
||||||
"then" "{" <t: AssertTree> "}" =>
|
"then" "{" <t: AssertTree> "}" =>
|
||||||
rsassert::Tree::If(Box::new(e), Box::new(t)),
|
rsassert::Tree::If(Box::new(e), Box::new(t)),
|
||||||
|
|
||||||
|
#[precedence(level="0")]
|
||||||
"if" <e: AssertExpression>
|
"if" <e: AssertExpression>
|
||||||
"then" "{" <t1: AssertTree> "}"
|
"then" "{" <t1: AssertTree> "}"
|
||||||
"else" "{" <t2: AssertTree> "}" =>
|
"else" "{" <t2: AssertTree> "}" =>
|
||||||
rsassert::Tree::IfElse(Box::new(e), Box::new(t1), Box::new(t2)),
|
rsassert::Tree::IfElse(Box::new(e), Box::new(t1), Box::new(t2)),
|
||||||
|
|
||||||
|
#[precedence(level="2")]
|
||||||
"let" <v: AssertVariable> <q: AssertQualifier?> "=" <e: AssertExpression> =>
|
"let" <v: AssertVariable> <q: AssertQualifier?> "=" <e: AssertExpression> =>
|
||||||
rsassert::Tree::Assignment(v, q, Box::new(e)),
|
rsassert::Tree::Assignment(v, q, Box::new(e)),
|
||||||
|
|
||||||
|
#[precedence(level="3")]
|
||||||
"return" <e: AssertExpression> =>
|
"return" <e: AssertExpression> =>
|
||||||
rsassert::Tree::Return(Box::new(e)),
|
rsassert::Tree::Return(Box::new(e)),
|
||||||
|
|
||||||
|
#[precedence(level="4")]
|
||||||
"for" <v: AssertVariable> "in" <r: AssertRange> "{" <t: AssertTree> "}" =>
|
"for" <v: AssertVariable> "in" <r: AssertRange> "{" <t: AssertTree> "}" =>
|
||||||
rsassert::Tree::For(v, r, Box::new(t)),
|
rsassert::Tree::For(v, r, Box::new(t)),
|
||||||
}
|
}
|
||||||
|
|
||||||
AssertVariable: rsassert::Variable = {
|
AssertVariable: rsassert::Variable = {
|
||||||
|
#[precedence(level="0")]
|
||||||
"label" => rsassert::Variable::Special(rsassert::Special::Label),
|
"label" => rsassert::Variable::Special(rsassert::Special::Label),
|
||||||
|
#[precedence(level="1")]
|
||||||
"edge" => rsassert::Variable::Special(rsassert::Special::Edge),
|
"edge" => rsassert::Variable::Special(rsassert::Special::Edge),
|
||||||
|
#[precedence(level="2")]
|
||||||
<v: Literal> => rsassert::Variable::Id(v),
|
<v: Literal> => rsassert::Variable::Id(v),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AssertExpression: rsassert::Expression = {
|
AssertExpression: rsassert::Expression = {
|
||||||
// Unary
|
// Unary
|
||||||
|
#[precedence(level="0")]
|
||||||
<unp: AssertUnaryPrefix> <e: AssertExpression> =>
|
<unp: AssertUnaryPrefix> <e: AssertExpression> =>
|
||||||
rsassert::Expression::Unary(unp, Box::new(e)),
|
rsassert::Expression::Unary(unp, Box::new(e)),
|
||||||
"(" <e: AssertExpression> ")" <uns: AssertUnarySuffix> =>
|
#[precedence(level="2")]
|
||||||
|
<e: AssertExpression> <uns: AssertUnarySuffix> =>
|
||||||
rsassert::Expression::Unary(uns, Box::new(e)),
|
rsassert::Expression::Unary(uns, Box::new(e)),
|
||||||
|
|
||||||
// binary
|
// binary
|
||||||
"(" <e1: AssertExpression> <b: AssertBinary> <e2: AssertExpression> ")" =>
|
#[precedence(level="3")] #[assoc(side="left")]
|
||||||
|
<e1: AssertExpression> <b: AssertBinary> <e2: AssertExpression> =>
|
||||||
rsassert::Expression::Binary(b, Box::new(e1), Box::new(e2)),
|
rsassert::Expression::Binary(b, Box::new(e1), Box::new(e2)),
|
||||||
|
#[precedence(level="1")]
|
||||||
<b: AssertBinaryPrefix>
|
<b: AssertBinaryPrefix>
|
||||||
"(" <e1: AssertExpression> "," <e2: AssertExpression> ")" =>
|
"(" <e1: AssertExpression> "," <e2: AssertExpression> ")" =>
|
||||||
rsassert::Expression::Binary(b, Box::new(e1), Box::new(e2)),
|
rsassert::Expression::Binary(b, Box::new(e1), Box::new(e2)),
|
||||||
|
|
||||||
|
#[precedence(level="4")]
|
||||||
"(" <e: AssertExpression> ")" => e,
|
"(" <e: AssertExpression> ")" => e,
|
||||||
"true" => rsassert::Expression::True,
|
"true" => rsassert::Expression::True,
|
||||||
"false" => rsassert::Expression::False,
|
"false" => rsassert::Expression::False,
|
||||||
|
|
||||||
|
#[precedence(level="5")]
|
||||||
<v: AssertVariable> => rsassert::Expression::Var(v),
|
<v: AssertVariable> => rsassert::Expression::Var(v),
|
||||||
|
|
||||||
// If changing IntegerType in assert.rs, also change from Num to another
|
// If changing IntegerType in assert.rs, also change from Num to another
|
||||||
// similar parser with different return type
|
// similar parser with different return type
|
||||||
|
#[precedence(level="6")]
|
||||||
<i: Num> => rsassert::Expression::Integer(i),
|
<i: Num> => rsassert::Expression::Integer(i),
|
||||||
|
|
||||||
|
#[precedence(level="7")]
|
||||||
<lab: AssertLabel> => rsassert::Expression::Label(Box::new(lab)),
|
<lab: AssertLabel> => rsassert::Expression::Label(Box::new(lab)),
|
||||||
<set: Set_of_entities> => rsassert::Expression::Set(set),
|
<set: Set_of_entities> => rsassert::Expression::Set(set),
|
||||||
"'" <el: Literal> "'" => rsassert::Expression::Element(translator.encode(el)),
|
"'" <el: Literal> "'" => rsassert::Expression::Element(translator.encode(el)),
|
||||||
|
|
||||||
// strings
|
// strings
|
||||||
|
#[precedence(level="8")]
|
||||||
PATH => rsassert::Expression::String(<>.trim_end_matches("\"")
|
PATH => rsassert::Expression::String(<>.trim_end_matches("\"")
|
||||||
.trim_start_matches("\"")
|
.trim_start_matches("\"")
|
||||||
.to_string()),
|
.to_string()),
|
||||||
@ -264,11 +373,13 @@ AssertUnaryPrefix: rsassert::Unary = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AssertUnarySuffix: rsassert::Unary = {
|
AssertUnarySuffix: rsassert::Unary = {
|
||||||
".empty" => rsassert::Unary::Empty,
|
#[precedence(level="0")]
|
||||||
".length" => rsassert::Unary::Length,
|
"." "empty" => rsassert::Unary::Empty,
|
||||||
".tostr" => rsassert::Unary::ToStr,
|
"." "length" => rsassert::Unary::Length,
|
||||||
".toel" => rsassert::Unary::ToEl,
|
"." "tostr" => rsassert::Unary::ToStr,
|
||||||
|
"." "toel" => rsassert::Unary::ToEl,
|
||||||
|
|
||||||
|
#[precedence(level="1")]
|
||||||
"." <q: AssertQualifier> => rsassert::Unary::Qualifier(q),
|
"." <q: AssertQualifier> => rsassert::Unary::Qualifier(q),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +488,6 @@ AssertLabel: RSlabel = {
|
|||||||
// system
|
// system
|
||||||
// a system is an environment, a set of entities as initial state, a context and
|
// a system is an environment, a set of entities as initial state, a context and
|
||||||
// a set of reaction rules.
|
// a set of reaction rules.
|
||||||
|
|
||||||
pub System: RSsystem = {
|
pub System: RSsystem = {
|
||||||
"Environment" ":" <delta: Environment>
|
"Environment" ":" <delta: Environment>
|
||||||
"Initial Entities" ":" <available_entities: Set>
|
"Initial Entities" ":" <available_entities: Set>
|
||||||
@ -392,7 +502,6 @@ pub System: RSsystem = {
|
|||||||
// experiment
|
// experiment
|
||||||
// an experiment is composed by a sequence of weights and a sequence of sets of
|
// an experiment is composed by a sequence of weights and a sequence of sets of
|
||||||
// entities of equal length.
|
// entities of equal length.
|
||||||
|
|
||||||
pub Experiment: (Vec<u32>, Vec<RSset>) = {
|
pub Experiment: (Vec<u32>, Vec<RSset>) = {
|
||||||
"Weights" ":" <w: Separated_Or<Num, ",">>
|
"Weights" ":" <w: Separated_Or<Num, ",">>
|
||||||
"Sets" ":" <s: Separated_Or<Set_of_entities, ",">>
|
"Sets" ":" <s: Separated_Or<Set_of_entities, ",">>
|
||||||
@ -749,14 +858,17 @@ Instruction: presets::Instruction = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub Run: presets::Instructions = {
|
pub Run: presets::Instructions = {
|
||||||
|
#[precedence(level="0")]
|
||||||
<sys: System> <instr: Separated_Or<Instruction, ",">> =>
|
<sys: System> <instr: Separated_Or<Instruction, ",">> =>
|
||||||
Instructions { system: presets::System::RSsystem { sys },
|
Instructions { system: presets::System::RSsystem { sys },
|
||||||
instructions: instr },
|
instructions: instr },
|
||||||
|
|
||||||
|
#[precedence(level="1")]
|
||||||
<sys: System> =>
|
<sys: System> =>
|
||||||
Instructions { system: presets::System::RSsystem { sys },
|
Instructions { system: presets::System::RSsystem { sys },
|
||||||
instructions: vec![] },
|
instructions: vec![] },
|
||||||
|
|
||||||
|
#[precedence(level="2")]
|
||||||
"Deserialize" "(" <path: Path> ")"
|
"Deserialize" "(" <path: Path> ")"
|
||||||
<instr: Separated_Or<Instruction, ",">> =>
|
<instr: Separated_Or<Instruction, ",">> =>
|
||||||
Instructions { system: presets::System::Deserialize { path },
|
Instructions { system: presets::System::Deserialize { path },
|
||||||
|
|||||||
@ -187,48 +187,98 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reformat_error<T, S>(
|
fn reformat_error<T, S>(
|
||||||
e: ParseError<usize, T, &'static str>
|
e: ParseError<usize, T, &'static str>,
|
||||||
|
input_str: &str,
|
||||||
) -> Result<S, String>
|
) -> Result<S, String>
|
||||||
where
|
where
|
||||||
T: Display,
|
T: Display,
|
||||||
{
|
{
|
||||||
match e {
|
match e {
|
||||||
ParseError::ExtraToken { token: (l, t, r) } => Err(format!(
|
ParseError::ExtraToken { token: (l, t, r) } => {
|
||||||
"Unexpected token \"{t}\" \
|
Err(format!(
|
||||||
|
"Unexpected token \"{t}\" \
|
||||||
between positions {l} and {r}."
|
between positions {l} and {r}."
|
||||||
)),
|
))
|
||||||
|
},
|
||||||
ParseError::UnrecognizedEof {
|
ParseError::UnrecognizedEof {
|
||||||
location: _,
|
location: _,
|
||||||
expected: _,
|
expected: _,
|
||||||
} => Err("End of file encountered while parsing.".into()),
|
} => {
|
||||||
|
Err("End of file encountered while parsing.".into())
|
||||||
|
},
|
||||||
ParseError::InvalidToken { location } => {
|
ParseError::InvalidToken { location } => {
|
||||||
Err(format!("Invalid token at position {location}."))
|
Err(format!("Invalid token at position {location}."))
|
||||||
}
|
}
|
||||||
ParseError::UnrecognizedToken {
|
ParseError::UnrecognizedToken {
|
||||||
token: (l, t, r),
|
token: (l, t, r),
|
||||||
expected,
|
expected: _,
|
||||||
} => {
|
} => {
|
||||||
|
use colored::Colorize;
|
||||||
|
|
||||||
let mut err = format!(
|
let mut err = format!(
|
||||||
"Unrecognized token \"{t}\" \
|
"Unrecognized token {}{}{} \
|
||||||
between positions {l} and {r}."
|
between positions {l} and {r}. ",
|
||||||
|
"\"".red(),
|
||||||
|
t.to_string().red(),
|
||||||
|
"\"".red(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Temporary debug.
|
// // Temporary debug.
|
||||||
err.push_str("Expected: ");
|
// err.push_str("Expected: ");
|
||||||
let mut it = expected.iter().peekable();
|
// let mut it = expected.iter().peekable();
|
||||||
while let Some(s) = it.next() {
|
// while let Some(s) = it.next() {
|
||||||
err.push('(');
|
// err.push('(');
|
||||||
err.push_str(s);
|
// err.push_str(&format!("{}", s.green()));
|
||||||
err.push(')');
|
// err.push(')');
|
||||||
if it.peek().is_some() {
|
// if it.peek().is_some() {
|
||||||
err.push(',');
|
// err.push(',');
|
||||||
err.push(' ');
|
// err.push(' ');
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
let right_new_line =
|
||||||
|
input_str[l..]
|
||||||
|
.find("\n")
|
||||||
|
.map(|pos| pos + l)
|
||||||
|
.unwrap_or(input_str.len());
|
||||||
|
let left_new_line =
|
||||||
|
input_str[..r].rfind("\n")
|
||||||
|
.map(|pos| pos + 1)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let line_number =
|
||||||
|
input_str[..l].match_indices('\n').count() + 1;
|
||||||
|
let pre_no_color = format!("{line_number} |");
|
||||||
|
let pre = format!("{}", pre_no_color.blue());
|
||||||
|
|
||||||
|
let line_pos_l = l - left_new_line;
|
||||||
|
let line_pos_r = r - left_new_line;
|
||||||
|
|
||||||
|
err.push_str(
|
||||||
|
&format!(".\nLine {} position {} to {}:\n{}{}{}{}",
|
||||||
|
line_number,
|
||||||
|
line_pos_l,
|
||||||
|
line_pos_r,
|
||||||
|
&pre,
|
||||||
|
&input_str[left_new_line..l].green(),
|
||||||
|
&input_str[l..r].red(),
|
||||||
|
&input_str[r..right_new_line],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
err.push('\n');
|
||||||
|
err.push_str(&" ".repeat(pre_no_color.len()-1));
|
||||||
|
err.push_str(&format!("{}", "|".blue()));
|
||||||
|
err.push_str(&" ".repeat(l - left_new_line));
|
||||||
|
err.push_str(&format!("{}", &"↑".red()));
|
||||||
|
if r - l > 2 {
|
||||||
|
err.push_str(&" ".repeat(r - l - 2));
|
||||||
|
err.push_str(&format!("{}", &"↑".red()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
},
|
},
|
||||||
ParseError::User { error } => Err(error.to_string()),
|
ParseError::User { error } => {
|
||||||
|
Err(error.to_string())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +288,7 @@ fn parser_experiment(
|
|||||||
) -> Result<(Vec<u32>, Vec<RSset>), String> {
|
) -> Result<(Vec<u32>, Vec<RSset>), String> {
|
||||||
match grammar::ExperimentParser::new().parse(translator, &contents) {
|
match grammar::ExperimentParser::new().parse(translator, &contents) {
|
||||||
Ok(sys) => Ok(sys),
|
Ok(sys) => Ok(sys),
|
||||||
Err(e) => reformat_error(e),
|
Err(e) => reformat_error(e, &contents),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +298,7 @@ fn parser_instructions(
|
|||||||
) -> Result<Instructions, String> {
|
) -> Result<Instructions, String> {
|
||||||
match grammar::RunParser::new().parse(translator, &contents) {
|
match grammar::RunParser::new().parse(translator, &contents) {
|
||||||
Ok(sys) => Ok(sys),
|
Ok(sys) => Ok(sys),
|
||||||
Err(e) => reformat_error(e),
|
Err(e) => reformat_error(e, &contents),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
225
testing/medical.system
Normal file
225
testing/medical.system
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
Environment: [
|
||||||
|
eafib1 = (?[{},{afib},{}]?.eafib1 + ?[{afib},{},{}]?.ehr),
|
||||||
|
ehr = (?[{},{heart_rate},{}]?.ehr + ?[{heart_rate},{},{}]?.ebb),
|
||||||
|
ebb = ({}.ebb + e_cbb + e_nsbb + e_sbb),
|
||||||
|
e_cbb = (?[{},{verapamil},{get_diltiazem}]?.empty
|
||||||
|
+ ?[{},{diltiazem},{get_verapamil}]?.empty),
|
||||||
|
e_nsbb = (?[{},{carvedilol},{get_propranolol}]?.empty
|
||||||
|
+ ?[{},{propranolol},{get_carvedilol}]?.empty),
|
||||||
|
e_sbb = (?[{},{atenolol},{get_bisoprolol}]?.empty
|
||||||
|
+ ?[{},{bisoprolol},{get_atenolol}]?.empty),
|
||||||
|
eafib2 = (?[{},{afib},{}]?.eafib2 + ?[{afib},{},{}]?.ehf),
|
||||||
|
ehf = (?[{},{has_fib},{}]?.ehf + ?[{has_fib},{},{}]?.eflec),
|
||||||
|
eflec = ({}.eflec + e_flec),
|
||||||
|
e_flec = {get_flecainide}.empty,
|
||||||
|
eafib3 = (?[{},{afib},{}]?.eafib3 + ?[{afib},{},{}]?.econs),
|
||||||
|
econs = (?[{},{heart_rate,has_fib},{}]?.econs
|
||||||
|
+ ?[{},{consensus_acei},{}]?.econs
|
||||||
|
+ ?[{consensus_acei,heart_rate},{},{}]?.estroke
|
||||||
|
+ ?[{consensus_acei,has_fib},{},{}]?.estroke),
|
||||||
|
estroke = (?[{},{diseases,over75},{}]?.ewarf
|
||||||
|
+ ?[{over75},{doac_fail,doac_int},{}]?.edoac
|
||||||
|
+ ?[{diseases},{doac_fail,doac_int},{}]?.edoac
|
||||||
|
+ ?[{over75,doac_fail},{},{}]?.evkant
|
||||||
|
+ ?[{over75,doac_int},{},{}]?.evkant
|
||||||
|
+ ?[{diseases,doac_fail},{},{}]?.evkant
|
||||||
|
+ ?[{diseases,doac_int},{},{}]?.evkant),
|
||||||
|
ewarf = ({}.ewarf + e_warf),
|
||||||
|
e_warf = {get_warfarin}.empty,
|
||||||
|
edoac = ({}.edoac + e_doac),
|
||||||
|
e_doac = (?[{},{dabigatran},{get_apixaban}]?.e_doacfail
|
||||||
|
+ ?[{},{apixaban},{get_dabigatran}]?.e_doacfail),
|
||||||
|
e_doacfail = (?[{doac_fail},{},{stop_doac}]?.evkant
|
||||||
|
+ ?[{},{doac_fail},{}]?.e_doacfail),
|
||||||
|
evkant = ({}.evkant + e_vkant),
|
||||||
|
e_vkant = {get_vkant}.empty,
|
||||||
|
ghyper = (?[{},{hyper},{}]?.ghyper + ?[{hyper},{},{}]?.g1),
|
||||||
|
g1 = (?[{diabete},{},{}]?.g2
|
||||||
|
+ ?[{below55},{diabete,origin},{}]?.g2
|
||||||
|
+ ?[{},{below55,diabete},{}]?.g3
|
||||||
|
+ ?[{origin},{diabete},{}]?.g3),
|
||||||
|
g2 = ({}.g2 + <1,e_acei>.g4 + <1,e_arb>.g5),
|
||||||
|
g3 = ({}.g3 + <1,e_cbb>.g6),
|
||||||
|
g4 = ({}.g4 + <1,e_cbb>.g7 + <1,e_td>.g8),
|
||||||
|
g5 = ({}.g5 + <1,e_cbb>.g9 + <1,e_td>.g10),
|
||||||
|
g6 = ({}.g6 + <1,e_acei>.g7 + <1,e_arb>.g9 + <1,e_td>.g11),
|
||||||
|
g7 = ({}.g7 + <1,e_arb>.etd + <1,e_td>.earb),
|
||||||
|
g8 = ({}.g8 + <1,e_arb>.ecbb + <1,e_cbb>.earb),
|
||||||
|
g9 = ({}.g9 + <1,e_acei>.etd + <1,e_td>.eacei),
|
||||||
|
g10 = ({}.g10 + <1,e_acei>.ecbb + <1,e_cbb>.eacei),
|
||||||
|
g11 = ({}.g11 + <1,e_acei>.earb + <1,e_arb>.eacei),
|
||||||
|
ecbb = ({}.ecbb + e_cbb),
|
||||||
|
eacei = ({}.eacei + e_acei),
|
||||||
|
e_acei = (?[{},{captopril},{get_benazepril}]?.empty
|
||||||
|
+ ?[{},{benazepril},{get_captopril}]?.empty),
|
||||||
|
earb = ({}.earb + e_arb),
|
||||||
|
e_arb = (?[{},{irbesartan},{get_olmesortan}]?.empty
|
||||||
|
+ ?[{},{olmesortan},{get_irbesartan}]?.empty),
|
||||||
|
etd = ({}.etd + e_td),
|
||||||
|
e_td = (?[{},{chlorothiazide},{get_indapamide}]?.empty
|
||||||
|
+ ?[{},{indapamide},{get_chlorothiazide}]?.empty),
|
||||||
|
k_doac = (?[{doac_test},{},{doac_ok}]?.empty
|
||||||
|
+ ?[{doac_test},{},{doac_fail}]?.empty
|
||||||
|
+ ?[{},{doac_test},{}]?.k_doac),
|
||||||
|
empty = {}.empty,
|
||||||
|
kafib = {afib}.empty,
|
||||||
|
khf = {has_fib}.empty,
|
||||||
|
khr = {heart_rate}.empty,
|
||||||
|
kcons = {consensus_acei}.empty,
|
||||||
|
kageA = {over75}.empty,
|
||||||
|
kageB = {below55}.empty,
|
||||||
|
kdiabete = {diabete}.empty,
|
||||||
|
kdoacint = {doac_int}.empty,
|
||||||
|
khyper = {hyper}.empty,
|
||||||
|
korigin = {origin}.empty
|
||||||
|
]
|
||||||
|
Initial Entities: {}
|
||||||
|
Context: [
|
||||||
|
eafib1,
|
||||||
|
eafib2,
|
||||||
|
eafib3,
|
||||||
|
ghyper,
|
||||||
|
kafib,
|
||||||
|
khf,
|
||||||
|
empty,
|
||||||
|
empty,
|
||||||
|
empty,
|
||||||
|
empty,
|
||||||
|
empty,
|
||||||
|
empty,
|
||||||
|
khyper,
|
||||||
|
empty,
|
||||||
|
k_doac
|
||||||
|
]
|
||||||
|
Reactions: (
|
||||||
|
[{hyper}, {}, {hyper}];
|
||||||
|
[{afib}, {}, {afib}];
|
||||||
|
[{has_fib}, {}, {has_fib}];
|
||||||
|
[{heart_rate}, {}, {heart_rate}];
|
||||||
|
[{consensus_acei}, {}, {consensus_acei}];
|
||||||
|
[{over75}, {}, {over75}];
|
||||||
|
[{below55}, {}, {below55}];
|
||||||
|
[{diabete}, {}, {diabete}];
|
||||||
|
[{origin}, {}, {origin}];
|
||||||
|
[{doac_int}, {}, {doac_int}];
|
||||||
|
[{doac}, {doac_ok,doac_fail}, {doac_test}];
|
||||||
|
[{doac_ok}, {doac_fail}, {doac_ok}];
|
||||||
|
[{doac_fail}, {doac_ok}, {doac_fail}];
|
||||||
|
[{hyper}, {}, {diseases}];
|
||||||
|
[{diabete}, {}, {diseases}];
|
||||||
|
[{get_diltiazem}, {stop_cbb}, {diltiazem,cbb}];
|
||||||
|
[{diltiazem}, {stop_cbb}, {diltiazem,cbb}];
|
||||||
|
[{get_verapamil}, {stop_cbb}, {verapamil,cbb}];
|
||||||
|
[{verapamil}, {stop_cbb}, {verapamil,cbb}];
|
||||||
|
[{diltiazem,verapamil}, {stop_cbb}, {alert_dup}];
|
||||||
|
[{get_propranolol}, {stop_nsbb}, {propranolol,nsbb}];
|
||||||
|
[{propranolol}, {stop_nsbb}, {propranolol,nsbb}];
|
||||||
|
[{get_carvedilol}, {stop_nsbb}, {carvedilol,nsbb}];
|
||||||
|
[{carvedilol}, {stop_nsbb}, {carvedilol,nsbb}];
|
||||||
|
[{propranolol,carvedilol}, {stop_nsbb}, {alert_dup}];
|
||||||
|
[{get_bisoprolol}, {stop_sbb}, {bisoprolol,sbb}];
|
||||||
|
[{bisoprolol}, {stop_sbb}, {bisoprolol,sbb}];
|
||||||
|
[{get_atenolol}, {stop_sbb}, {atenolol,sbb}];
|
||||||
|
[{atenolol}, {stop_sbb}, {atenolol,sbb}];
|
||||||
|
[{bisoprolol,atenolol}, {stop_sbb}, {alert_dup}];
|
||||||
|
[{get_flecainide}, {stop_flec}, {flecainide}];
|
||||||
|
[{flecainide}, {stop_flec}, {flecainide}];
|
||||||
|
[{get_warfarin}, {stop_warf}, {warfarin}];
|
||||||
|
[{warfarin}, {stop_warf}, {warfarin}];
|
||||||
|
[{get_apixaban}, {stop_doac}, {apixaban,doac}];
|
||||||
|
[{apixaban}, {stop_doac}, {apixaban,doac}];
|
||||||
|
[{get_dabigatran}, {stop_doac}, {dabigatran,doac}];
|
||||||
|
[{dabigatran}, {stop_doac}, {dabigatran,doac}];
|
||||||
|
[{apixaban,dabigatran}, {stop_doac}, {alert_dup}];
|
||||||
|
[{get_vkant}, {stop_vkant}, {vkant}];
|
||||||
|
[{vkant}, {stop_vkant}, {vkant}];
|
||||||
|
[{get_benazepril}, {stop_acei}, {benazepril,acei}];
|
||||||
|
[{benazepril}, {stop_acei}, {benazepril,acei}];
|
||||||
|
[{get_captopril}, {stop_acei}, {captopril,acei}];
|
||||||
|
[{captopril}, {stop_acei}, {captopril,acei}];
|
||||||
|
[{benazepril,captopril}, {stop_acei}, {alert_dup}];
|
||||||
|
[{get_olmesortan}, {stop_arb}, {olmesortan,arb}];
|
||||||
|
[{olmesortan}, {stop_arb}, {olmesortan,arb}];
|
||||||
|
[{get_irbesartan}, {stop_arb}, {irbesartan,arb}];
|
||||||
|
[{irbesartan}, {stop_arb}, {irbesartan,arb}];
|
||||||
|
[{olmesortan,irbesartan}, {stop_arb}, {alert_dup}];
|
||||||
|
[{get_indapamide}, {stop_td}, {indapamide,td}];
|
||||||
|
[{indapamide}, {stop_td}, {indapamide,td}];
|
||||||
|
[{get_chlorothiazide}, {stop_td}, {chlorothiazide,td}];
|
||||||
|
[{chlorothiazide}, {stop_td}, {chlorothiazide,td}];
|
||||||
|
[{indapamide,chlorothiazide}, {stop_td}, {alert_dup}];
|
||||||
|
[{doac,doac_fail}, {stop_doac}, {doac_danger}];
|
||||||
|
[{doac,doac_danger}, {stop_doac}, {danger}];
|
||||||
|
[{get_apixaban,get_diltiazem}, {}, {moderate}];
|
||||||
|
[{get_apixaban,diltiazem}, {}, {moderate}];
|
||||||
|
[{apixaban,get_diltiazem}, {}, {moderate}];
|
||||||
|
[{apixaban,diltiazem}, {}, {moderate}];
|
||||||
|
[{get_apixaban,get_verapamil}, {}, {moderate}];
|
||||||
|
[{get_apixaban,verapamil}, {}, {moderate}];
|
||||||
|
[{apixaban,get_verapamil}, {}, {moderate}];
|
||||||
|
[{apixaban,verapamil}, {}, {moderate}];
|
||||||
|
[{get_dabigatran,get_diltiazem}, {}, {moderate}];
|
||||||
|
[{get_dabigatran,diltiazem}, {}, {moderate}];
|
||||||
|
[{dabigatran,get_diltiazem}, {}, {moderate}];
|
||||||
|
[{dabigatran,diltiazem}, {}, {moderate}];
|
||||||
|
[{get_dabigatran,get_verapamil}, {}, {major}];
|
||||||
|
[{get_dabigatran,verapamil}, {}, {major}];
|
||||||
|
[{dabigatran,get_verapamil}, {}, {major}];
|
||||||
|
[{dabigatran,verapamil}, {}, {major}];
|
||||||
|
[{get_dabigatran,get_carvedilol}, {}, {moderate}];
|
||||||
|
[{get_dabigatran,carvedilol}, {}, {moderate}];
|
||||||
|
[{dabigatran,get_carvedilol}, {}, {moderate}];
|
||||||
|
[{dabigatran,carvedilol}, {}, {moderate}];
|
||||||
|
[{get_warfarin,get_benazepril}, {}, {minor}];
|
||||||
|
[{get_warfarin,benazepril}, {}, {minor}];
|
||||||
|
[{warfarin,get_benazepril}, {}, {minor}];
|
||||||
|
[{warfarin,benazepril}, {}, {minor}];
|
||||||
|
[{get_warfarin,get_indapamide}, {}, {minor}];
|
||||||
|
[{get_warfarin,indapamide}, {}, {minor}];
|
||||||
|
[{warfarin,get_indapamide}, {}, {minor}];
|
||||||
|
[{warfarin,indapamide}, {}, {minor}];
|
||||||
|
[{get_warfarin,get_chlorothiazide}, {}, {minor}];
|
||||||
|
[{get_warfarin,chlorothiazide}, {}, {minor}];
|
||||||
|
[{warfarin,get_chlorothiazide}, {}, {minor}];
|
||||||
|
[{warfarin,chlorothiazide}, {}, {minor}];
|
||||||
|
[{get_warfarin,get_propranolol}, {}, {minor}];
|
||||||
|
[{get_warfarin,propranolol}, {}, {minor}];
|
||||||
|
[{warfarin,get_propranolol}, {}, {minor}];
|
||||||
|
[{warfarin,propranolol}, {}, {minor}];
|
||||||
|
[{get_flecainide,get_diltiazem}, {}, {major}];
|
||||||
|
[{get_flecainide,diltiazem}, {}, {major}];
|
||||||
|
[{flecainide,get_diltiazem}, {}, {major}];
|
||||||
|
[{flecainide,diltiazem}, {}, {major}];
|
||||||
|
[{get_flecainide,get_verapamil}, {}, {major}];
|
||||||
|
[{get_flecainide,verapamil}, {}, {major}];
|
||||||
|
[{flecainide,get_verapamil}, {}, {major}];
|
||||||
|
[{flecainide,verapamil}, {}, {major}];
|
||||||
|
[{get_flecainide,get_bisoprolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,bisoprolol}, {}, {moderate}];
|
||||||
|
[{flecainide,get_bisoprolol}, {}, {moderate}];
|
||||||
|
[{flecainide,bisoprolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,get_atenolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,atenolol}, {}, {moderate}];
|
||||||
|
[{flecainide,get_atenolol}, {}, {moderate}];
|
||||||
|
[{flecainide,atenolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,get_propranolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,propranolol}, {}, {moderate}];
|
||||||
|
[{flecainide,get_propranolol}, {}, {moderate}];
|
||||||
|
[{flecainide,propranolol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,get_carvedilol}, {}, {moderate}];
|
||||||
|
[{get_flecainide,carvedilol}, {}, {moderate}];
|
||||||
|
[{flecainide,get_carvedilol}, {}, {moderate}];
|
||||||
|
[{flecainide,carvedilol}, {}, {moderate}];
|
||||||
|
[{major}, {}, {major}];
|
||||||
|
[{moderate}, {}, {moderate}];
|
||||||
|
[{minor}, {}, {minor}];
|
||||||
|
[{alert_dup}, {}, {alert_dup}];
|
||||||
|
[{danger},{},{danger}]
|
||||||
|
)
|
||||||
|
|
||||||
|
Digraph > Dot
|
||||||
|
| Hide
|
||||||
|
| Hide
|
||||||
|
| ! "white"
|
||||||
|
| ! "black"
|
||||||
|
> Save("out.dot")
|
||||||
Reference in New Issue
Block a user