Search code examples
c#kicad

Parsing S-Expression in C# (KiCAD PCB file)


I'm writing a .NET API (C#) to manage KiCAD PCB files. Their format according to docs (here) is a sort of S-Expression. I tried reusing some S-Expression parsers but for several reasons they could not fit my needs so I decided to write one but I found myself stuck. As a first try I wrote down a simple function that recursively descend into the the file structure and create a System.Windows.Forms.TreeNode hierarchy that match the structure (I used TreeNode because I use a TreeView component to depict the parsed structure):

    private TreeNode Parser(StreamReader srStream, TreeNode tnCurrentNode)
    {
        bool _string = false;

        do
        {
            TreeNode _tnNode = null;
            char _c;

            _c = (char)srStream.Read();

            if (_string)
            {
                tnCurrentNode.Text += _c;
                if (_c == '"')
                    _string = false;
            }
            else
                switch (_c)
                {
                    case '(':
                        _tnNode = new TreeNode();
                        tnCurrentNode.Nodes.Add(Parser(srStream, _tnNode));
                        break;

                    case ')':
                        return tnCurrentNode;
                    case '\n':
                    case '\r':
                        break;

                    case '"':
                        tnCurrentNode.Text += _c;
                        _string = true;
                        break;

                    default:
                        tnCurrentNode.Text += _c;
                        break;
                }
        } while (!srStream.EndOfStream);

        return tnCurrentNode;
    }

After that I wrote down a serializer to write back files. Everything works fine but there's a case that's improperly handled by my parser and to which I've not been able to find a proper solution:

(fp_text value V23105 (at -2 0 180) (layer F.SilkS) hide
  (effects (font (size 1 1) (thickness 0.25)))
)

The hide token position is not correctly managed (it cannot be serialized in the original position). The reason is simple: while the parser correctly handles subnodes, since they starts with an opening bracket, it simply ignores values that are at the same level (i.e. values separated by white spaces) such as hide option. How can I manage such condition? I tried in several ways but I just got into a number of stack overflow exceptions (I just lost control of recursion).

In the meanwhile I defined a custom class to handle nodes (to be used instead of TreeNode):

public class KiCADNode
{
    public string Value { get; set; }
    public NodeType Type { get; set; }

    private readonly List<KiCADNode> _Nodes = new List<KiCADNode>();
    public ICollection<KiCADNode> Nodes { get { return _Nodes; } }

    public static implicit operator TreeNode(KiCADNode node)
    {
        if (node == null)
            return null;

        TreeNode _treenode = new TreeNode();

        _treenode.Text = node.ToString();

        foreach (KiCADNode _node in node._Nodes)
            _treenode.Nodes.Add((TreeNode)_node);

        return _treenode;
    }

    public override string ToString()
    {
        StringBuilder _sb = new StringBuilder();

        if (Type == NodeType.List)
            _sb.Append('(');

        _sb.Append(Value);

        if (Type == NodeType.Atom)
            _sb.Append(' ');

        if (Type == NodeType.List)
            _sb.Append(')');

        return _sb.ToString();
    }
    public KiCADNode()
    {
        Type = NodeType.Atom;
    }

    public KiCADNode(NodeType type)
    {
        Type = type;
    }

    public KiCADNode(string value) : this()
    {
        Value = value;
    }

    public KiCADNode(string value, NodeType type) : this(value)
    {
        Type = type;
    }

    private static KiCADNode Parse(StreamReader input)
    {
        KiCADNode _node = new KiCADNode("PCB");
        return Parser(input, _node);
    }

    private static KiCADNode Parser(StreamReader input, KiCADNode current_node)
    {
        bool _string = false;

        while (!input.EndOfStream)
        {
            KiCADNode _new_node = null;
            char _c;

            _c = (char)input.Read();

            if (_string)
            {
                current_node.Value += _c;
                if (_c == '"')
                    _string = false;
            }
            else
                switch (_c)
                {
                    case '(':
                        _new_node = new KiCADNode(NodeType.List);
                        current_node.Nodes.Add(Parser(input, _new_node));
                        break;

                    case ')':
                        return current_node;
                    case '\n':
                    case '\r':
                        break;

                    case '"':
                        current_node.Value += _c;
                        _string = true;
                        break;

                    default:
                        current_node.Value += _c;
                        break;
                }
        } 

        return current_node;
    }

    public static KiCADNode Parse(string filename)
    {
        if (!File.Exists(filename))
            return null;

        using (StreamReader _input = new StreamReader(filename))
        {
            return Parse(_input);
        }
    }
}

Solution

  • This is a common parsing idiom. In EBNF

    node :: atom | "(" list ")"

    list ::= node | list node

    which in C# one could implement as an abstract base class, and a class for node, atom and list. I did something similar here https://github.com/bobc/eakit/tree/master/source/kicad_tools/SExpression