https://git.spwbk.site/swatson/modmark/raw/master/pl_proto.pl
___________________________________
#!/usr/bin/perl

use strict;
use warnings;
use Data::Dumper;
use lib "/home/swatson/perl5/lib/perl5";
use GraphViz2;

my $SOURCE_FILE = $ARGV[0];

sub read_to_var($) {
       my $file_path = shift;
       my $content;
       open(my $fh, '<', $file_path) or die "cannot open file $file_path";
       {
               local $/;
               $content = <$fh>;
       }
       close($fh);

       return $content;
}

my $src_content = read_to_var($SOURCE_FILE);

sub trim($) {
       my $str = shift;
       $str =~ s/^\s+|\s+$//g;
       return $str;
}

my @module_library_paths = (
       ".",
);
my %modules;

sub parse_module_file {
       my $src = shift;

       my %module;


       my $last_proc_type = "null";
       my $last_input = "null";
       my $last_output = "null";
       foreach my $line ( split("\n", $src) ) {
               chomp $line;
               if ( $line =~ m/^#/ ) {
                       next;
               }

               if ( $line =~ m/^Manufacturer:(.*)/ ) {
                       my $manu = $1;
                       $manu = trim($manu);
                       $module{'Manufacturer'} = $manu;
               }

               if ( $line =~ m/^Module:(.*)/ ) {
                       my $mod = $1;
                       $mod = trim($mod);
                       $module{'Module'} = $mod;
               }

               if ( $line =~ m/^Revision:(.*)/ ) {
                       my $rev = $1;
                       $rev = trim($rev);
                       $module{'Rev'} = $rev;
               }

               if ( $line =~ m/^-\ / ) {
                       if ( $line =~ m/^-\ Input:(.*)/ ) {
                               my $input = $1;
                               $input = trim($input);
                               my %input_chars;
                               $last_input = $input;
                               $last_proc_type = "input";
                               $module{'Inputs'}->{$input} = \%input_chars;
                       }

                       if ( $line =~ m/^-\ Knob:(.*)/ ) {
                               my $knob = $1;
                               $knob = trim($knob);
                               my %knob_chars;
                               $last_input = $knob;
                               $last_proc_type = "input";
                               $module{'Inputs'}->{$knob} = \%knob_chars;
                       }

                       if ( $line =~ m/^-\ Output:(.*)/ ) {
                               my $output = $1;
                               $output = trim($output);
                               my %output_chars;
                               $last_output = $output;
                               $last_proc_type = "output";
                               $module{'Outputs'}->{$output} = \%output_chars;
                       }

                       if ( $line =~ m/^-\ Button:(.*)/ ) {
                               my $button = $1;
                               $button = trim($button);
                               my %button_chars;
                               $last_input = $button;
                               $last_proc_type = "input";
                               $module{'Inputs'}->{$button} = \%button_chars;
                       }

               }

               if ( $line =~ m/^--\ / ) {
                       if ( $line =~ m/^--\ Position:(.*)/ ) {
                               my $pos_args = $1;
                               $pos_args = trim($pos_args);
                               if ( $last_proc_type eq "input" ) {
                                       $module{'Inputs'}->{$last_input}->{'pos'} = $pos_args;
                               } elsif ( $last_proc_type eq "output" ) {
                                       $module{'Outputs'}->{$last_output}->{'pos'} = $pos_args;
                               }
                       }
               }

       }

       return \%module;
}

my %AST;

my %PARSE_TABLE = (
       'comment' => '^#.*$',
       'title' => '^Title: (.*)$',
       'mod_path' => '^ModuleDir\ "(.*)"$',
       'import' => '^import (Module)::([a-zA-Z0-9]{1,})::([a-zA-Z0-9]{1,})(.*$)',
       'set' => '^set\ (.*)$',
       'connect' => '^connect(.*)$',
);

my %PARSE_RULES = (
       'comment' => sub {
               # Do nothing, throw this line out
       },
       'title' => sub {
               my $title = shift;
               $AST{'Title'} = $title;
       },
       'mod_path' => sub {
               my $file_path = shift;
               if ( ! -d $file_path ) {
                       die "Path: $file_path doesn't look like a directory, exiting";
               }

               push(@module_library_paths, $file_path);
       },
       'import' => sub {
               my $module_import = shift;
               my $import_manu = shift;
               my $import_mod = shift;
               my $import_as = shift;
               my @module_files = sub {
                       my @files;
                       foreach my $path ( @module_library_paths ) {
                               my @f = split("\n", `find $path`);
                               foreach my $file ( @f ) {
                                       if ( $file =~ m/.module$/ ) {
                                               my $f_bn = `basename $file`;
                                               chomp $f_bn;
                                               if ( ! grep(/$f_bn/, @files) ) {
                                                       push(@files, $file);
                                               }
                                       }
                               }
                       }
                       return @files;
               }->();
               foreach my $mod_file ( @module_files ) {
                       my $mod_file_content = read_to_var($mod_file);
                       my $mod_ref = parse_module_file($mod_file_content);

                       if ( $import_mod eq $$mod_ref{'Module'} ) {

                               if ( defined $AST{'Modules'} ) {
                                       my $r = grep { $import_mod eq $_->{'Module'} } @{$AST{'Modules'}};
                                       if ( $r == 0 ) {
                                               push(@{$AST{'Modules'}}, $mod_ref);
                                       }
                               } else {
                                       push(@{$AST{'Modules'}}, $mod_ref);
                               }
                       } else {
                               next;
                       }
               }
       },
       'set' => sub {
               my $set_line = shift;
               my $mod_to_set;
               my $attr_to_set;
               my $attr_param;
               my $value;
               my $setter = sub {
                       my $mod_to_set = shift;
                       my $attr_to_set = shift;
                       my $attr_param = shift;
                       if ( $attr_param eq "position" ) {
                               $attr_param = "pos";
                       }
                       my $value = shift;

                       my %set_params = (
                               'Param' => $attr_param,
                               'Value' => $value,
                       );

                       # Check values against mod definition
                       # Pull mod ref out of AST for straight forward checking
                       my $mod_ref;
                       # Check we have module in AST
                       my $r = grep { $mod_to_set eq $_->{'Module'} } @{$AST{'Modules'}};
                       if ( $r eq 0 ) {
                               die "Can't set value on module that is not imported: $mod_to_set\n";
                       } else {
                               foreach my $module_ref ( @{$AST{'Modules'}} ) {
                                       if ( $mod_to_set eq $$module_ref{'Module'} ) {
                                               $mod_ref = $module_ref;
                                               last;
                                       }
                               }
                       }

                       # Check that module has param we want to set
                       if ( ! $attr_to_set eq $$mod_ref{'Inputs'}->{$attr_to_set} ) {
                               die "Can't set a param that doesn't existing in the module spec: $attr_to_set";
                       }

                       # If the set has an attr param, check that it's in the allowed range on the attr
                       if ( $attr_param ne "null" ) {
                               my $attr_range = $$mod_ref{'Inputs'}->{$attr_to_set}->{'pos'};
                               if ( $attr_range =~ m/([0-9]{1,2})\-([0-9]{1,2})/ ) {
                                       my $r_begin = $1;
                                       my $r_end = $2;
                                       if ( $value > $r_end || $value < $r_begin ) {
                                               die "Parse error: attr_param value: $value for $attr_to_set : $attr_param is outside of range: $r_begin $r_end";
                                       }
                               } else {
                                       die "Somehow encountered parse error in setter for module file $$mod_ref{'Module'}\n";
                               }
                       }

                       $AST{'Sets'}->{$mod_to_set}->{$attr_to_set} = \%set_params;
               };

               if ( $set_line =~ m/(^[A-Z]{1}[A-Za-z0-9]{1,})\.{1}([A-Za-z0-9]{1,})\ \=\ (.*)$/ ) {
                       $mod_to_set = $1;
                       $attr_to_set = $2;
                       $value = $3;
               } elsif ( $set_line =~ m/(^[A-Z]{1}[A-Za-z]{1,})\.{1}([A-Za-z0-9]{1,})\.([A-Za-z0-9]{1,})\ \=\ (.*)$/ ) {
                       $mod_to_set = $1;
                       $attr_to_set = $2;
                       $attr_param = $3;
                       $value = $4;
               } else {
                       die "Parse error at $set_line";
               }

               if ( ! defined $attr_param || $attr_param eq "" ) {
                       $attr_param = "null",
               };

               $setter->($mod_to_set,$attr_to_set,$attr_param,$value);
       },
       'connect' => sub {
               my $connect_line = shift;
               $connect_line = trim($connect_line);
               if ( $connect_line =~ m/([A-Z]{1}[A-Za-z0-9]{1,})\.([A-Za-z0-9]{1,})\ ([A-Z]{1}[A-Za-z0-9]{1,})\.([A-Za-z0-9]{1,})/ ) {
                       my $output_mod = $1;
                       my $output_mod_port = $2;
                       my $input_mod = $3;
                       my $input_mod_dst = $4;

                       # Check we have modules defined
                       if ( ! defined $AST{'Modules'} ) {
                               die "Parse error: Encountered connection but no modules are imported: $connect_line"
                       } else {
                               # Check that connect references imported modules and get module refs
                               my $output_mod_ref;
                               my $input_mod_ref;
                               foreach my $mod_ref ( @{$AST{'Modules'}} ) {
                                       if ( $$mod_ref{'Module'} eq $output_mod ) {
                                               $output_mod_ref = $mod_ref;
                                       } elsif ( $$mod_ref{'Module'} eq $input_mod ) {
                                               $input_mod_ref = $mod_ref;
                                       }
                               }
                               # If we reach the end of the loop and input/output refs are not set
                               # we didn't find the module, and it's a parse error
                               if ( ! defined $output_mod_ref ||
                                    $output_mod_ref eq "" ||
                                    ! defined $input_mod_ref ||
                                    $input_mod_ref eq "" ) {
                                       die "Parse error, couldn't find $output_mod or $input_mod in AST"
                               }

                               # Check src/dst ports
                               if ( ! defined $$output_mod_ref{'Outputs'}->{$output_mod_port} ) {
                                       die "Parse error: $output_mod_port is not defined in module $output_mod"
                               } elsif ( ! defined $$input_mod_ref{'Inputs'}->{$input_mod_dst} ) {
                                       die "Parse error: $input_mod_dst is not defined in module $input_mod"
                               }

                               # Everything looks good, make connection
                               my $get_conn_id = sub {
                                       if ( ! defined $AST{'Connections'} ) {
                                               return 0;
                                       } else {
                                               my $c = 0;
                                               foreach my $conn_id ( keys %{$AST{'Connections'}} ) {
                                                       $c++;
                                               }
                                               return $c;
                                       }
                               };
                               my $conn_id = $get_conn_id->();
                               my %conn_map = (
                                       'Output_Module' => $output_mod,
                                       'Output_Port' => $output_mod_port,
                                       'Input_Module' => $input_mod,
                                       'Input_Mod_Dst' => $input_mod_dst,
                               );
                               $AST{'Connections'}->{$conn_id} = \%conn_map;

                       }
               } else {
                       die "Parse error at $connect_line";
               }
       },

);

# Basic line parser
sub line_parse($) {
       my $line = shift;
       my $line_type = "null";
       my @line_caps;
       foreach my $key ( keys %PARSE_TABLE ) {
               if ( $line =~ m/$PARSE_TABLE{$key}/ ) {
                       $line_type = $key;
               }
       }

       if ( $line_type ne "null" && $line =~ m/$PARSE_TABLE{$line_type}/) {
               if ( defined $1 && ! defined $2 ) {
                       $PARSE_RULES{$line_type}->($1);
               } elsif ( defined $1 && defined $2 && defined $3 && defined $4 ) {
                       # This is for `import`
                       $PARSE_RULES{$line_type}->($1,$2,$3,$4);
               } else {
                       $PARSE_RULES{$line_type}->();
               }
       } else {
               print("$line\n");
       }
}

# For testing module definitions
if ( defined $ARGV[1] && $ARGV[1] eq "--import-test" ) {
       my $mod_ref = parse_module_file($src_content);
       print Dumper $mod_ref;
       exit 0;
}

# MAIN starts here - split the input and parse it
foreach my $line ( split("\n", $src_content) ) {
       chomp $line;
       if ( $line eq "" ) {
               next;
       }
       line_parse($line);
}

# Dump the AST for now. This is where we'd render the output
print Dumper %AST;

my $graph = GraphViz2->new(
       edge   => {color => 'grey'},
       global => {directed => 1},
       graph  => {label => $AST{'Title'}, rankdir => 'TB'},
       node   => {shape => 'square'},
);

# Given a mod_ref, construct a graph object
# that represents a module
sub module_node_constructor($$) {
       my $graph = shift;
       my $mod_ref = shift;
       $graph->push_subgraph(
               name => "cluster_$$mod_ref{'Module'}",
               graph => {label => "$$mod_ref{'Module'}"},
               node => {color => 'black', shape => 'circle'},
       );

       # $graph->push_subgraph(
       #               name => "cluster_mod_info",
       #               graph => {label => "mod_info"},
       #               node => {color => 'grey', shape => 'square'},
       # );
       # foreach my $node_name ( keys %{$mod_ref} ) {
       #       if ( $node_name =~ m/Module|Manufacturer|Rev/ ) {
       #               $graph->add_node(name => "$node_name\n: $$mod_ref{$node_name}", shape => 'square');
       #       }
       # }

       # $graph->pop_subgraph;

       if ( defined $AST{'Sets'}->{$$mod_ref{'Module'}} ) {
               my $sets_ref = $AST{'Sets'}->{$$mod_ref{'Module'}};
               foreach my $set ( keys %{$sets_ref} ) {
                       my $name = "cluster" . "_" . "$set";
                       my $label = "$set" . "_" . "settings";
                       $graph->push_subgraph(
                               name => $name,
                               graph => {label => $label},
                               node => {color => "green", shape => 'square'},
                       );
                       $graph->add_node(name => "$set : $$sets_ref{$set}->{'Param'} : $$sets_ref{$set}->{'Value'}");
                       $graph->pop_subgraph;
               }
       }

       # Handle connections
       if ( defined $AST{'Connections'} ) {
               my $conn_ref = $AST{'Connections'};
               foreach my $conn ( keys %{$conn_ref} ) {
                       if ($$mod_ref{'Module'} eq $$conn_ref{$conn}->{'Output_Module'} ) {
                               my $name = "cluster" . "_" . "$$conn_ref{$conn}->{'Output_Port'}_$conn";
                               my $label = "$$conn_ref{$conn}->{'Output_Port'}_$conn";
                               $graph->push_subgraph(
                                       name => $name,
                                       graph => {label => $label},
                                       node => {color => "blue"},
                               );
                               $graph->add_node(name => "$$conn_ref{$conn}->{'Output_Port'}");
                               $graph->pop_subgraph;
                       } elsif ($$mod_ref{'Module'} eq $$conn_ref{$conn}->{'Input_Module'} ) {
                               my $name = "cluster" . "_" . "$$conn_ref{$conn}->{'Input_Mod_Dst'}_$conn";
                               my $label = "$$conn_ref{$conn}->{'Input_Mod_Dst'}_$conn";
                               $graph->push_subgraph(
                                       name => $name,
                                       graph => {label => $label},
                                       node => {color => 'purple'},
                               );
                               $graph->add_node(name => "$$conn_ref{$conn}->{'Input_Mod_Dst'}");
                               $graph->pop_subgraph;
                       }

               }
       }

       $graph->pop_subgraph;
}

# Draw modules
foreach my $mod_ref ( @{$AST{'Modules'}} ) {
       module_node_constructor($graph,$mod_ref);
}

# Draw connections
foreach my $conn_ref ( keys %{$AST{'Connections'}} ) {
       my $from = $AST{'Connections'}->{$conn_ref}->{'Output_Port'};
       my $to = $AST{'Connections'}->{$conn_ref}->{'Input_Mod_Dst'};
       $graph->add_edge(color => 'red', from => $from, to => $to);
}

my $format = 'svg';
$graph->run(format => $format, output_file => "test.svg");