generated from racklet/base-repo-layout
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from racklet/kicad-evaluator
Implement the KiCad expression evaluator (for real this time)
- Loading branch information
Showing
10 changed files
with
372 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,25 @@ | ||
use std::collections::HashMap; | ||
use kicad_rs::error::DynamicResult; | ||
use kicad_rs::eval; | ||
use kicad_rs::parser::{parse_schematic, SchematicFile}; | ||
use std::env; | ||
use std::error::Error; | ||
use std::path::Path; | ||
|
||
use evalexpr::Context; | ||
|
||
use kicad_rs::resolver; | ||
use kicad_rs::resolver::*; | ||
|
||
// Main function, can return different kinds of errors | ||
fn main() -> Result<(), Box<dyn Error>> { | ||
let mut args: Vec<String> = env::args().collect(); | ||
let p = std::path::Path::new(args.get(1).ok_or("expected file as first argument")?); | ||
let updated = evaluate_schematic(&p)?; | ||
// print!("{}", updated); | ||
fn main() -> DynamicResult<()> { | ||
let args: Vec<String> = env::args().collect(); | ||
let path = std::path::Path::new(args.get(1).ok_or("expected file as first argument")?); | ||
|
||
let mut input = HashMap::new(); | ||
input.insert("a", "5"); | ||
input.insert("d", "b * 2"); | ||
input.insert("b", "a + c"); | ||
input.insert("c", "6"); | ||
// Load the schematic file and parse it | ||
let file = SchematicFile::load(path)?; | ||
let mut schematic = parse_schematic(&file, String::new())?; | ||
|
||
let mut expr = HashMap::<String, Expression>::new(); | ||
for (k, v) in input { | ||
expr.insert(String::from(k), Expression::new(v.into(), String::new())); | ||
} | ||
// Index the parsed schematic and use the index to evaluate it. The | ||
// index links to the schematic using mutable references, so that's | ||
// why the schematic itself needs to be passed in as mutable here. | ||
let mut index = eval::index_schematic(&mut schematic)?; | ||
eval::evaluate_schematic(&mut index)?; | ||
|
||
let c = resolver::resolve(&expr); | ||
println!("{:?}", c.get_value("d")); | ||
// TODO: Apply the internal schematic back to kicad_parse_gen::schematic::Schematic and print | ||
println!("{:#?}", index); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn evaluate_schematic(p: &Path) -> Result<String, Box<dyn Error>> { | ||
// Read the schematic using kicad_parse_gen | ||
let schematic = kicad_parse_gen::read_schematic(p)?; | ||
|
||
// Walk through all components in the sheet | ||
for comp in schematic.components() { | ||
// Require comp.name to be non-empty | ||
// if comp.name.is_empty() { | ||
// return Err(Box::new(errorf("Every component must have a name"))); | ||
// } | ||
|
||
// Walk through all the fields | ||
for f in comp.fields.iter().filter(|&f| is_expression(&f.name)) { | ||
println!("{}: {}", comp.reference, f.name); | ||
} | ||
} | ||
|
||
Ok(schematic.to_string()) | ||
} | ||
|
||
fn is_expression(s: &String) -> bool { | ||
s.ends_with("_expr") || s.ends_with("_expression") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
mod entry; | ||
mod index; | ||
mod path; | ||
|
||
use crate::error::{errorf, DynamicResult}; | ||
use crate::eval::index::{ComponentIndex, Node, SheetIndex}; | ||
use crate::eval::path::Path; | ||
use crate::types::Schematic; | ||
|
||
pub fn index_schematic(sch: &mut Schematic) -> DynamicResult<SheetIndex> { | ||
let mut index = SheetIndex::new(); | ||
|
||
for c in sch.components.iter_mut() { | ||
let mut component_idx = ComponentIndex::new(); | ||
for a in c.attributes.iter_mut() { | ||
if component_idx.contains_key(&a.name) { | ||
return Err(errorf(&format!( | ||
"duplicate attribute definition: {}", | ||
a.name | ||
))); | ||
} | ||
component_idx.insert(a.name.clone(), a.into()); | ||
} | ||
index | ||
.map | ||
.insert(c.labels.reference.clone(), Node::Component(component_idx)); | ||
} | ||
|
||
for sub_sch in sch.sub_schematics.iter_mut() { | ||
if index.map.contains_key(&sub_sch.id) { | ||
return Err(errorf(&format!( | ||
"component and schematic name collision: {}", | ||
sub_sch.id | ||
))); | ||
} | ||
index | ||
.map | ||
.insert(sub_sch.id.clone(), Node::Sheet(index_schematic(sub_sch)?)); | ||
} | ||
|
||
Ok(index) | ||
} | ||
|
||
pub fn evaluate_schematic(index: &mut SheetIndex) -> DynamicResult<()> { | ||
// Perform resolving recursively in depth-first order | ||
for node in index.map.values_mut() { | ||
if let Node::Sheet(sub_index) = node { | ||
evaluate_schematic(sub_index)?; | ||
} | ||
} | ||
|
||
// Collect all attributes for all components | ||
let mut paths = Vec::new(); | ||
for (node_ref, node) in index.map.iter() { | ||
if let Node::Component(component_index) = node { | ||
for a in component_index.keys() { | ||
paths.push(vec![node_ref.into(), a.into()].into()) | ||
} | ||
} | ||
} | ||
|
||
// Evaluate all the collected attributes | ||
for path in paths.iter() { | ||
evaluate(index, path)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn evaluate(idx: &mut SheetIndex, p: &Path) -> DynamicResult<()> { | ||
let entry = idx | ||
.resolve_entry(p.iter()) | ||
.ok_or(errorf("entry not found"))?; | ||
|
||
if entry.value_defined()? { | ||
return Ok(()); // Don't update if already set | ||
} | ||
|
||
let node = evalexpr::build_operator_tree(entry.get_expression())?; | ||
for dep in node.iter_variable_identifiers().map(|id| id.into()) { | ||
evaluate(idx, &dep)?; | ||
} | ||
|
||
let value = node.eval_with_context(idx)?; | ||
idx.update_entry(p.iter(), value)?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
use crate::error::{errorf, DynamicResult}; | ||
use crate::types; | ||
use crate::types::Attribute; | ||
use evalexpr::{EvalexprError, EvalexprResult, Value, ValueType}; | ||
use std::cell::RefCell; | ||
|
||
#[derive(Debug)] | ||
pub struct Entry<'a> { | ||
set_in_progress: RefCell<bool>, | ||
attribute: &'a mut Attribute, | ||
value: Option<Value>, | ||
} | ||
|
||
// This (slightly modified) function's origin is for some reason marked as private for | ||
// external consumers in evalexpr, even though all the type-specific variants are exposed. | ||
fn expected_type(expected_type: &ValueType, actual: Value) -> EvalexprError { | ||
match expected_type { | ||
ValueType::String => EvalexprError::expected_string(actual), | ||
ValueType::Int => EvalexprError::expected_int(actual), | ||
ValueType::Float => EvalexprError::expected_float(actual), | ||
ValueType::Boolean => EvalexprError::expected_boolean(actual), | ||
ValueType::Tuple => EvalexprError::expected_tuple(actual), | ||
ValueType::Empty => EvalexprError::expected_empty(actual), | ||
} | ||
} | ||
|
||
impl<'a> Entry<'a> { | ||
pub fn get_name(&self) -> &str { | ||
&self.attribute.name | ||
} | ||
|
||
pub fn get_expression(&self) -> &str { | ||
&self.attribute.expression | ||
} | ||
|
||
pub fn get_value(&self) -> Option<&Value> { | ||
self.value.as_ref() | ||
} | ||
|
||
pub fn update(&mut self, value: Value) -> EvalexprResult<Option<Value>> { | ||
*self.set_in_progress.borrow_mut() = false; | ||
|
||
let mut str = value.to_string(); | ||
if let Some(unit) = self.attribute.unit.as_ref() { | ||
str.push(' '); | ||
str.push_str(unit); | ||
} | ||
|
||
self.attribute.value = types::Value::parse(str); | ||
if let Some(t) = self.value.as_ref().map(|v| ValueType::from(v)) { | ||
if t != ValueType::from(&value) { | ||
return Err(expected_type(&t, value)); | ||
} | ||
} | ||
|
||
Ok(self.value.replace(value)) | ||
} | ||
|
||
pub fn value_defined(&self) -> DynamicResult<bool> { | ||
if *self.set_in_progress.borrow() { | ||
// TODO: More precise error reporting | ||
return Err(errorf("dependency loop detected")); | ||
} | ||
|
||
*self.set_in_progress.borrow_mut() = true; | ||
Ok(self.value.is_some()) | ||
} | ||
} | ||
|
||
impl<'a> From<&'a mut Attribute> for Entry<'a> { | ||
fn from(attribute: &'a mut Attribute) -> Self { | ||
Self { | ||
set_in_progress: RefCell::new(false), | ||
attribute, | ||
value: None, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use crate::eval::entry::Entry; | ||
use crate::eval::path::Path; | ||
use evalexpr::{Context, ContextWithMutableVariables, EvalexprError, EvalexprResult, Value}; | ||
use std::collections::HashMap; | ||
|
||
// The default attribute is signified by an empty string | ||
const DEFAULT_ATTR: String = String::new(); | ||
|
||
pub type ComponentIndex<'a> = HashMap<String, Entry<'a>>; | ||
|
||
#[derive(Default, Debug)] | ||
pub struct SheetIndex<'a> { | ||
pub(crate) map: HashMap<String, Node<'a>>, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub enum Node<'a> { | ||
Sheet(SheetIndex<'a>), | ||
Component(ComponentIndex<'a>), | ||
} | ||
|
||
impl<'a> SheetIndex<'a> { | ||
pub fn new() -> Self { | ||
Default::default() | ||
} | ||
|
||
pub fn resolve_entry<'b>( | ||
&self, | ||
mut path: impl ExactSizeIterator<Item = &'b String>, | ||
) -> Option<&Entry> { | ||
self.map | ||
.get(path.next()?) | ||
.map(|n| match n { | ||
Node::Sheet(idx) => idx.resolve_entry(path), | ||
Node::Component(idx) => { | ||
if path.len() > 1 { | ||
None // There's more elements, an incomplete path was given | ||
} else { | ||
idx.get(path.next().unwrap_or(&DEFAULT_ATTR)) | ||
} | ||
} | ||
}) | ||
.flatten() | ||
} | ||
|
||
pub fn update_entry<'b>( | ||
&mut self, | ||
mut path: impl ExactSizeIterator<Item = &'b String>, | ||
value: Value, | ||
) -> EvalexprResult<Option<Value>> { | ||
match self | ||
.map | ||
.get_mut(path.next().ok_or(err("path exhausted"))?) | ||
.ok_or(err("entry not found"))? | ||
{ | ||
Node::Sheet(idx) => idx.update_entry(path, value), | ||
Node::Component(idx) => { | ||
if path.len() > 1 { | ||
Err(err("component encountered during traversal")) | ||
} else { | ||
idx.get_mut(path.next().unwrap_or(&DEFAULT_ATTR)) | ||
.ok_or(err("attribute not found"))? | ||
.update(value) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl<'a> Context for SheetIndex<'a> { | ||
fn get_value(&self, identifier: &str) -> Option<&Value> { | ||
self.resolve_entry(Path::from(identifier).iter()) | ||
.map(|e| e.get_value()) | ||
.flatten() | ||
} | ||
|
||
fn call_function(&self, _identifier: &str, _argument: &Value) -> EvalexprResult<Value> { | ||
// TODO: Fixed function set (voltage divider etc.) | ||
unimplemented!("functions are currently unsupported"); | ||
} | ||
} | ||
|
||
impl<'a> ContextWithMutableVariables for SheetIndex<'a> { | ||
fn set_value(&mut self, identifier: String, value: Value) -> EvalexprResult<()> { | ||
self.update_entry(Path::from(identifier).iter(), value) | ||
.map(|_| ()) | ||
} | ||
} | ||
|
||
fn err(msg: &str) -> EvalexprError { | ||
EvalexprError::CustomMessage(msg.into()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use std::fmt; | ||
use std::slice::Iter; | ||
|
||
const PATH_SEPARATOR: &str = "."; | ||
|
||
#[derive(Debug)] | ||
pub struct Path { | ||
components: Vec<String>, | ||
} | ||
|
||
impl Path { | ||
pub(crate) fn iter(&self) -> Iter<'_, String> { | ||
self.components.iter() | ||
} | ||
} | ||
|
||
impl From<String> for Path { | ||
fn from(s: String) -> Self { | ||
let components = s.split(PATH_SEPARATOR).map(|s| s.into()).collect(); | ||
Self { components } | ||
} | ||
} | ||
|
||
impl From<&str> for Path { | ||
fn from(s: &str) -> Self { | ||
let components = s.split(PATH_SEPARATOR).map(|s| s.into()).collect(); | ||
Self { components } | ||
} | ||
} | ||
|
||
impl From<Vec<String>> for Path { | ||
fn from(v: Vec<String>) -> Self { | ||
let components = v.into_iter().filter(|s| !s.is_empty()).collect(); | ||
Self { components } | ||
} | ||
} | ||
|
||
impl fmt::Display for Path { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", self.components.join(PATH_SEPARATOR)) | ||
} | ||
} |
Oops, something went wrong.