#!/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");