Skip to content

Commit

Permalink
Merge pull request #14 from racklet/kicad-evaluator
Browse files Browse the repository at this point in the history
Implement the KiCad expression evaluator (for real this time)
  • Loading branch information
luxas authored Jun 21, 2021
2 parents a2b1ba7 + e6deed4 commit 9821be1
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 132 deletions.
64 changes: 16 additions & 48 deletions tools/kicad_rs/src/bin/evaluator.rs
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")
}
2 changes: 1 addition & 1 deletion tools/kicad_rs/src/classifier.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::DynamicResult;
use crate::labels::LabelsMatch;
use crate::requirements::Requirement;
use crate::error::DynamicResult;
use crate::types::{Component, Schematic};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
Expand Down
4 changes: 2 additions & 2 deletions tools/kicad_rs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub struct StringError {
str: String,
}

pub fn errorf(s: &str) -> StringError {
StringError { str: s.into() }
pub fn errorf(s: &str) -> Box<dyn Error> {
Box::new(StringError { str: s.into() })
}

impl fmt::Display for StringError {
Expand Down
88 changes: 88 additions & 0 deletions tools/kicad_rs/src/eval.rs
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(())
}
78 changes: 78 additions & 0 deletions tools/kicad_rs/src/eval/entry.rs
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,
}
}
}
92 changes: 92 additions & 0 deletions tools/kicad_rs/src/eval/index.rs
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())
}
42 changes: 42 additions & 0 deletions tools/kicad_rs/src/eval/path.rs
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))
}
}
Loading

0 comments on commit 9821be1

Please sign in to comment.