├── src
├── python
│ ├── __init__.py
│ ├── plugins
│ │ ├── __init__.py
│ │ └── cfg_plotter.py
│ ├── dump.py
│ ├── core.py
│ └── om.py
├── parser
│ ├── location.ml
│ ├── location.mli
│ ├── parser.messages
│ └── dune
├── analysis
│ ├── dune
│ ├── unused_variable.mli
│ ├── use_define.mli
│ ├── declaration_analysis.mli
│ ├── cyclomatic_complexity.mli
│ ├── cyclomatic_complexity.ml
│ ├── unused_variable.ml
│ ├── declaration_analysis.ml
│ └── use_define.ml
├── lib
│ ├── plcopen_cp2.mli
│ ├── zerodiv.mli
│ ├── plcopen_cp1.mli
│ ├── plcopen_cp4.mli
│ ├── plcopen_n3.mli
│ ├── plcopen_cp9.mli
│ ├── plcopen_cp3.mli
│ ├── dune
│ ├── plcopen_cp25.mli
│ ├── plcopen_l17.mli
│ ├── plcopen_cp6.mli
│ ├── plcopen_cp8.mli
│ ├── plcopen_cp13.mli
│ ├── plcopen_l10.mli
│ ├── checkerLib.mli
│ ├── plcopen_cp28.mli
│ ├── plcopen_l10.ml
│ ├── plcopen_l17.ml
│ ├── plcopen_cp3.ml
│ ├── plcopen_cp6.ml
│ ├── plcopen_cp9.ml
│ ├── plcopen_cp8.ml
│ ├── plcopen_cp28.ml
│ ├── plcopen_cp2.ml
│ ├── plcopen_cp13.ml
│ ├── zerodiv.ml
│ ├── plcopen_cp1.ml
│ ├── plcopen_cp25.ml
│ ├── checkerLib.ml
│ ├── plcopen_n3.ml
│ └── plcopen_cp4.ml
├── core
│ ├── dune
│ ├── warn_output.mli
│ ├── plcopen.mli
│ ├── tok_info.mli
│ ├── sel.mli
│ ├── warn.mli
│ ├── dump.mli
│ ├── tok_info.ml
│ ├── warn_output.ml
│ ├── warn.ml
│ ├── config.ml
│ ├── config.mli
│ ├── env.mli
│ ├── dump.ml
│ ├── common.ml
│ ├── env.ml
│ ├── ast_util.mli
│ ├── cfg.mli
│ ├── sel.ml
│ └── ast_util.ml
└── bin
│ ├── dune
│ └── iec_checker.ml
├── test
├── st
│ ├── good
│ │ ├── user2.st
│ │ ├── time-literals.st
│ │ ├── comments.st
│ │ ├── array-use.st
│ │ ├── empty-body.st
│ │ ├── array-in-loop.st
│ │ ├── references.st
│ │ ├── struct-declaration.st
│ │ ├── function-blocks.st
│ │ ├── multiple-pous.st
│ │ ├── function-declaration.st
│ │ ├── direct-variables.st
│ │ ├── case-insensitive.st
│ │ ├── simple.st
│ │ ├── variables-declaration.st
│ │ ├── user1.st
│ │ ├── multiple-variables.st
│ │ ├── generic-types.st
│ │ ├── configurations.st
│ │ ├── types.st
│ │ ├── literals.st
│ │ ├── control-statements.st
│ │ └── user3.st
│ ├── merge-1.st
│ ├── bad
│ │ ├── semantic-error.st
│ │ └── lexing-error.st
│ ├── merge-2.st
│ ├── plcopen-cp1.st
│ ├── zero-division.st
│ ├── plcopen-cp4.st
│ ├── plcopen-cp25.st
│ ├── plcopen-n3.st
│ ├── plcopen-cp3.st
│ ├── plcopen-l17.st
│ ├── plcopen-l10.st
│ ├── plcopen-cp8.st
│ ├── plcopen-cp28.st
│ ├── plcopen-cp6.st
│ ├── plcopen-cp13.st
│ ├── dead-code.st
│ ├── declaration-analysis.st
│ └── plcopen-cp9.st
├── selxml
│ ├── SEL_RTAC
│ │ ├── ProjSpace_GVL1.xml
│ │ ├── System
│ │ │ └── Main Controller.xml
│ │ ├── ProjSpace_Minimal.xml
│ │ ├── ProjSpace_Example.xml
│ │ ├── ProjSpace_Simple.xml
│ │ └── ProjSpace_MAIN_POU.xml
│ ├── Project Info.xml
│ ├── POUs
│ │ ├── POUs Space
│ │ │ ├── POUSpace_GVL.xml
│ │ │ ├── POUSpace_Minimal.xml
│ │ │ ├── POUSpace_Example.xml
│ │ │ ├── POUSpace_Simple.xml
│ │ │ └── POUSpace_MAIN_POU.xml
│ │ └── Project Information.xml
│ └── Navigator Layout.xml
├── test_zerodiv.py
├── test_plcopen_xml.py
├── test_unused_variable.py
├── test_core.py
├── test_merge_files.py
├── test_use_define.py
├── test_declaration_analysis.py
├── test_selxml.py
├── test_cfa.py
├── test_plcopen.py
└── test_parser.py
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── .github
└── workflows
│ ├── pycodestyle.yml
│ └── unit_tests.yml
├── Makefile
├── dune-project
├── iec_checker.opam
├── checker.py
├── .gitignore
├── README.md
└── LICENSE
/src/python/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/st/good/user2.st:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/python/plugins/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | ijson==3.0.3
2 | pytest==7.2.*
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | ijson==3.0.3
2 | graphviz==0.14
3 | pygraphviz==1.5
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pycodestyle]
2 | max-line-length = 100
3 | statistics = True
4 |
--------------------------------------------------------------------------------
/src/parser/location.ml:
--------------------------------------------------------------------------------
1 | type loc = Lexing.position * Lexing.position
2 |
3 |
--------------------------------------------------------------------------------
/test/st/merge-1.st:
--------------------------------------------------------------------------------
1 | TYPE example_struct :
2 | STRUCT
3 | Field : BOOL;
4 | END_STRUCT
5 | END_TYPE
6 |
--------------------------------------------------------------------------------
/test/st/bad/semantic-error.st:
--------------------------------------------------------------------------------
1 | TYPE
2 | ANALOG_DATA_OK: INT (0 .. 16);
3 | ANALOG_DATA_BAD: INT ("foo" .. "bar");
4 | END_TYPE
5 |
--------------------------------------------------------------------------------
/test/st/good/time-literals.st:
--------------------------------------------------------------------------------
1 | PROGRAM program0
2 | VAR
3 | A : INT;
4 | T : INT;
5 | END_VAR
6 |
7 | A := 1;
8 | END_PROGRAM
9 |
--------------------------------------------------------------------------------
/src/parser/location.mli:
--------------------------------------------------------------------------------
1 | (** Describes locations of symbols in concrete syntax tree. *)
2 |
3 | type loc = Lexing.position * Lexing.position
4 |
5 |
--------------------------------------------------------------------------------
/test/st/merge-2.st:
--------------------------------------------------------------------------------
1 | PROGRAM Program1
2 | VAR
3 | instance AT %MW500 : example_struct;
4 | END_VAR
5 |
6 | %MW500 := TRUE;
7 | END_PROGRAM
8 |
--------------------------------------------------------------------------------
/src/analysis/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name IECCheckerAnalysis)
3 | (public_name iec_checker.analysis)
4 | (synopsis "Common semantic checks")
5 | (libraries core iec_checker.core))
6 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp2.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP2: All code shall be used in the application *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : Cfg.t list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/zerodiv.mli:
--------------------------------------------------------------------------------
1 | (** ZeroDivision errors *)
2 |
3 | open IECCheckerCore
4 | module S = IECCheckerCore.Syntax
5 |
6 | val do_check : S.iec_library_element list -> Warn.t list
7 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp1.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP1 – Access to a member shall be by name *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp4.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP4: Direct addressing should not overlap *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/analysis/unused_variable.mli:
--------------------------------------------------------------------------------
1 | (** Detect unused variables in the source code. *)
2 | open IECCheckerCore
3 | module S = Syntax
4 |
5 | val run : S.iec_library_element list -> Warn.t list
6 |
--------------------------------------------------------------------------------
/src/lib/plcopen_n3.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-N3 – Define the names to avoid *)
2 | open IECCheckerCore
3 | module S = IECCheckerCore.Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp9.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP9 – Limit the complexity of POU code. *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Cfg.t list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp3.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP3: All variables shall be initialized before being used *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp1.st:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP1 – Access to a member shall be by name *)
2 |
3 | PROGRAM program0
4 | VAR
5 | head AT %B0 : INT;
6 | END_VAR
7 | %B0 := 42;
8 | END_PROGRAM
9 |
10 |
--------------------------------------------------------------------------------
/src/lib/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name IECCheckerLib)
3 | (public_name iec_checker.lib)
4 | (synopsis "Implementation of the static analysis rules")
5 | (libraries core iec_checker.core iec_checker.analysis))
6 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp25.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP25: Data type conversion should be explicit. *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Env.t list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/analysis/use_define.mli:
--------------------------------------------------------------------------------
1 | (* Detect common errors in 'use' occurrences of the local variables in POUs. *)
2 | open IECCheckerCore
3 | module S = Syntax
4 |
5 | val run : S.iec_library_element list -> Warn.t list
6 |
--------------------------------------------------------------------------------
/src/lib/plcopen_l17.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-L17 – Each IF instruction should have an ELSE clause *)
2 | open IECCheckerCore
3 | module S = IECCheckerCore.Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp6.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP6: Avoid external variables in functions, function blocks and classes *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp8.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP8: Floating point comparison shall not be equality or inequality *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp13.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP13 – POUs shall not call themselves directly or indirectly *)
2 | open IECCheckerCore
3 | module S = IECCheckerCore.Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/src/lib/plcopen_l10.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-L10 – Usage of CONTINUE and EXIT instruction should be avoid *)
2 | open IECCheckerCore
3 | module S = IECCheckerCore.Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
--------------------------------------------------------------------------------
/test/st/bad/lexing-error.st:
--------------------------------------------------------------------------------
1 | PROGRAM program0
2 | VAR
3 | LocalVar0 : DINT;
4 | LocalVar1 : TOD;
5 | _LocalVar2 : DINT;
6 | END_VAR
7 |
8 | LocalVar0 := 42;
9 |
10 | wtf?
11 | END_PROGRAM
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/checkerLib.mli:
--------------------------------------------------------------------------------
1 | open IECCheckerCore
2 | module S = Syntax
3 |
4 | val run_all_checks : S.iec_library_element list -> Env.t list -> Cfg.t list -> bool -> Warn.t list
5 | (** [run_all_checks] Run all available checks *)
6 |
--------------------------------------------------------------------------------
/test/st/zero-division.st:
--------------------------------------------------------------------------------
1 | PROGRAM program0
2 | VAR_ACCESS
3 | acc : Var1 : DINT;
4 | acc : Var2 : DINT;
5 | END_VAR
6 |
7 | Var1 := 19 / 0;
8 | Var2 := Var1 / 1;
9 | Var2 := Var2 / 0;
10 | END_PROGRAM
11 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp28.mli:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-CP28: Time and physical measures comparisons shall not be equality or inequality *)
2 | open IECCheckerCore
3 | module S = Syntax
4 | val do_check : S.iec_library_element list -> Warn.t list
5 |
6 |
--------------------------------------------------------------------------------
/.github/workflows/pycodestyle.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 |
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 |
7 | steps:
8 | - name: Python Style Checker
9 | uses: andymckay/pycodestyle-action@0.1.3
10 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp4.st:
--------------------------------------------------------------------------------
1 | FUNCTION demo : INT
2 | VAR_INPUT
3 | x1 AT %MX40 : INT; (* PLCOPEN-CP4 *)
4 | x2 AT %MX41 : INT; (* PLCOPEN-CP4 *)
5 | x3 AT %MX510 : INT;
6 | END_VAR
7 | x1 := 42;
8 | END_FUNCTION
9 |
--------------------------------------------------------------------------------
/test/st/good/comments.st:
--------------------------------------------------------------------------------
1 | PROGRAM comments
2 | VAR
3 | INITIAL : BOOL := FALSE;
4 | X1 : INT := 10;
5 | Y1 : INT := 20;
6 | // Test
7 | END_VAR
8 | // Test1
9 | Y1 := Z + 1; // Test2
10 | O_I1 := Y1; // Test3
11 | END_PROGRAM
12 |
--------------------------------------------------------------------------------
/test/st/good/array-use.st:
--------------------------------------------------------------------------------
1 | FUNCTION_BLOCK MAIN_POU
2 |
3 | VAR
4 | i:INT;
5 | state: INT:=0;
6 | END_VAR
7 |
8 | IF gvl.CALL_LIST_INTERMEDIATE_GO_UP[i]= TRUE THEN
9 | state:=100;
10 | END_IF
11 |
12 | END_FUNCTION_BLOCK
13 |
--------------------------------------------------------------------------------
/src/analysis/declaration_analysis.mli:
--------------------------------------------------------------------------------
1 | (** Declaration analysis: inspect compatibility of the identifiers and their
2 | declarations. *)
3 | open IECCheckerCore
4 | module S = Syntax
5 |
6 | val run : S.iec_library_element list -> Env.t list -> Warn.t list
7 |
--------------------------------------------------------------------------------
/test/st/good/empty-body.st:
--------------------------------------------------------------------------------
1 | (* Allow POU that contains only {} in their bodies. *)
2 | FUNCTION EmptyFun : BOOL
3 | {}
4 | END_FUNCTION
5 |
6 | FUNCTION_BLOCK EmptyFB
7 | {}
8 | END_FUNCTION_BLOCK
9 |
10 | PROGRAM EmptyProgram
11 | {}
12 | END_PROGRAM
13 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp25.st:
--------------------------------------------------------------------------------
1 | PROGRAM demo
2 | VAR
3 | I : INT := 10;
4 | J : REAL := 0.55;
5 | END_VAR
6 |
7 | I := J; (* PLCOPEN-CP25 *)
8 | J := I; (* PLCOPEN-CP25 *)
9 | I := REAL_TO_INT(J);
10 | J := INT_TO_REAL(I);
11 | END_PROGRAM
12 |
--------------------------------------------------------------------------------
/src/core/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name IECCheckerCore)
3 | (public_name iec_checker.core)
4 | (synopsis "The core of iec-checker")
5 | (libraries core yojson xmlm)
6 | (preprocess
7 | (pps ppxlib ppx_fields_conv ppx_deriving.std ppx_deriving.show
8 | ppx_deriving_yojson)))
9 |
--------------------------------------------------------------------------------
/test/st/plcopen-n3.st:
--------------------------------------------------------------------------------
1 | PROGRAM program0
2 | VAR
3 | FOR_B : INT; (* PLCOPEN-N3 *)
4 | NO_FALSE_POSITIVE : INT;
5 | IF_A : INT; (* PLCOPEN-N3 *)
6 | TOF : INT; (* PLCOPEN-N3 *)
7 | OK_ALLOWED : INT;
8 | END_VAR
9 |
10 | FOR_B := IF_A / 1;
11 | END_PROGRAM
12 |
--------------------------------------------------------------------------------
/src/core/warn_output.mli:
--------------------------------------------------------------------------------
1 | (** Output interfaces for static analysis warnings. *)
2 | module W = Warn
3 |
4 | type output_format =
5 | | Plain
6 | | Json
7 |
8 | val print_report : W.t list -> output_format -> unit
9 | (** [print_report] Print warnings in selected format to stdout. *)
10 |
--------------------------------------------------------------------------------
/src/analysis/cyclomatic_complexity.mli:
--------------------------------------------------------------------------------
1 | (** Routines to evaluate cyclomatic complexity for intraprocedural control flow
2 | graph. *)
3 | open IECCheckerCore
4 | module S = Syntax
5 |
6 | val eval_mccabe : Cfg.t -> int
7 | (** [eval_mccabe cfg] Evaluate McCabe cyclomatic complexity for [cfg]. *)
8 |
--------------------------------------------------------------------------------
/test/st/good/array-in-loop.st:
--------------------------------------------------------------------------------
1 | PROGRAM Simple
2 |
3 | VAR
4 | i : DINT;
5 | arr1: ARRAY [1..2] OF BOOL;
6 | END_VAR
7 |
8 | VAR
9 | unused_var AT %IW1.2 : REAL := 200.0;
10 | END_VAR
11 |
12 | IF ARR1[i] < 10 THEN
13 | i := i + 1;
14 | END_IF;
15 | END_PROGRAM
16 |
--------------------------------------------------------------------------------
/src/parser/parser.messages:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 |
3 | grammar: PROGRAM IDENTIFIER END_PROGRAM
4 |
5 | Either a declaration or '%%' is expected at this point.
6 |
7 | # ----------------------------------------------------------------------------
8 |
--------------------------------------------------------------------------------
/src/core/plcopen.mli:
--------------------------------------------------------------------------------
1 | (** Module to reconstruct IEC61131-3 source code from the PLCOpen XML schema. *)
2 | module S = Syntax
3 |
4 | val reconstruct_from_channel : in_channel -> string
5 | (** [reconstruct_from_channel channel] Reconstruct source code from the the
6 | input [channel]. Return complete source code listing from parsed schema. *)
7 |
--------------------------------------------------------------------------------
/test/st/good/references.st:
--------------------------------------------------------------------------------
1 | FUNCTION_BLOCK fb1
2 | VAR_INPUT
3 | vi1 : DINT;
4 | END_VAR
5 | v1 := 42;
6 | END_FUNCTION_BLOCK
7 |
8 | PROGRAM example
9 | VAR
10 | temp : DINT;
11 | END_VAR
12 |
13 | temp ?= temp^;
14 | temp ?= temp^^;
15 | temp ?= temp^^^;
16 | temp ?= NULL;
17 | temp ?= fb1;
18 | END_PROGRAM
19 |
--------------------------------------------------------------------------------
/test/st/good/struct-declaration.st:
--------------------------------------------------------------------------------
1 | TYPE example_struct :
2 | STRUCT
3 | X : DINT;
4 | Y : BOOL;
5 | Z : STRING[40];
6 | END_STRUCT
7 | END_TYPE
8 |
9 | PROGRAM Program1
10 | VAR
11 | instance AT %MW500 : example_struct;
12 | END_VAR
13 |
14 | // Write the first character of Z:
15 | %MW504 := 'E';
16 | END_PROGRAM
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build test clean
2 |
3 | default: build
4 |
5 | build:
6 | dune build @install
7 | @test -L bin || ln -s _build/install/default/bin .
8 |
9 | test: build
10 | @/bin/bash -c "source venv/bin/activate; \
11 | pushd test >/dev/null; \
12 | pytest; \
13 | popd >/dev/null; "
14 |
15 | clean:
16 | dune clean
17 | git clean -dfXq --exclude=\!venv/**
18 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/ProjSpace_GVL1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 | ProjSpace_GVL1
9 |
13 |
14 |
--------------------------------------------------------------------------------
/src/core/tok_info.mli:
--------------------------------------------------------------------------------
1 | (** Describes single token of a parse tree *)
2 |
3 | type t = { id : int; linenr : int; col : int } [@@deriving yojson, show]
4 | (** Parse tree item *)
5 |
6 | val create : Lexing.lexbuf -> t
7 | (** [create] Create new parse tree element from Lexing.lexbuf *)
8 |
9 | val create_dummy : unit -> t
10 | (** [create_dummy] Create a new dummy parse tree element *)
11 |
12 | val to_string : t -> string
13 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp3.st:
--------------------------------------------------------------------------------
1 | FUNCTION demo : INT
2 | VAR_INPUT
3 | x1 : INT := 0;
4 | x2 : INT; (* PLCOPEN-CP3 *)
5 | x3 : STRING[5]; (* PLCOPEN-CP3 *)
6 | x4 : WSTRING[25] := 'spaces_disallowed';
7 | x5 : ARRAY [1..2, 1..3] OF INT; (* PLCOPEN-CP3 *)
8 | x6: ARRAY [0..7] OF BOOL := [0,1,1,0,0,1,0,0];
9 | x7 AT %B0 : INT;
10 | END_VAR
11 | x1 := 42;
12 | END_FUNCTION
13 |
--------------------------------------------------------------------------------
/src/bin/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (public_name iec_checker)
3 | (libraries core clap re iec_checker.lib iec_checker.parser
4 | iec_checker.core iec_checker.analysis)
5 | ; Enable for debugging:
6 | ; (modes byte)
7 | (preprocess
8 | (pps ppx_variants_conv ppx_jane)))
9 |
10 | (env
11 | (dev
12 | (flags
13 | ; Make warnings non-fatal
14 | (:standard -warn-error -A)))
15 | (release
16 | (ocamlopt_flags :standard -ccopt -static)))
17 |
--------------------------------------------------------------------------------
/test/st/plcopen-l17.st:
--------------------------------------------------------------------------------
1 | (** PLCOPEN-L17 – Each IF instruction should have an ELSE clause
2 |
3 | Reference: PLCopen Coding Guidelines 6.5.9. *)
4 |
5 | PROGRAM program0
6 | VAR
7 | a : INT;
8 | END_VAR
9 |
10 | IF (a = 42) (* PLCOPEN-L17 *)
11 | THEN
12 | a := 0;
13 | END_IF;
14 |
15 | IF (a = 42) (* no warning *)
16 | THEN
17 | a := 0;
18 | ELSE
19 | a := 19;
20 | END_IF;
21 |
22 | END_PROGRAM
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/plcopen_l10.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = IECCheckerCore.Syntax
5 |
6 | let do_check elems =
7 | Ast_util.get_stmts elems
8 | |> List.fold_left
9 | ~init:[]
10 | ~f:(fun acc s -> begin
11 | match s with
12 | | S.StmContinue ti | S.StmExit ti -> acc @ [Warn.mk ti.linenr ti.col "PLCOPEN-L10" "Usage of CONTINUE and EXIT instruction should be avoid"]
13 | | _ -> acc
14 | end)
15 |
--------------------------------------------------------------------------------
/test/st/good/function-blocks.st:
--------------------------------------------------------------------------------
1 | FUNCTION_BLOCK fb0
2 | END_FUNCTION_BLOCK
3 |
4 | FUNCTION_BLOCK fb1
5 | VAR_INPUT
6 | vi1 : DINT;
7 | END_VAR
8 | VAR_OUTPUT
9 | vo1 : DINT;
10 | END_VAR
11 | VAR_IN_OUT
12 | vio1 : DINT;
13 | END_VAR
14 | VAR
15 | v1 : DINT;
16 | END_VAR
17 | VAR_TEMP
18 | vt1 : DINT;
19 | END_VAR
20 | VAR RETAIN
21 | vr1 : DINT;
22 | END_VAR
23 | VAR NON_RETAIN
24 | vnr1 : DINT;
25 | END_VAR
26 | v1 := 42;
27 | END_FUNCTION_BLOCK
28 |
--------------------------------------------------------------------------------
/test/st/plcopen-l10.st:
--------------------------------------------------------------------------------
1 | PROGRAM l10
2 | VAR
3 | i : INT := 0;
4 | j : INT := 0;
5 | flag : INT := 1;
6 | counter : INT := 0;
7 | END_VAR
8 |
9 | FOR i := 0 TO 10 DO
10 | FOR j := 10 TO 100 BY 2 DO
11 | IF flag THEN
12 | EXIT; (* PLCOPEN-CP10 *)
13 | END_IF;
14 | counter := counter + 1;
15 | IF j = 10 THEN
16 | CONTINUE; (* PLCOPEN-CP10 *)
17 | END_IF;
18 | EXIT; (* PLCOPEN-CP10 *)
19 | END_FOR;
20 | END_FOR;
21 | END_PROGRAM
22 |
23 |
--------------------------------------------------------------------------------
/src/parser/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name IECCheckerParser)
3 | (public_name iec_checker.parser)
4 | (synopsis "Provides an interface for processing the IEC61131-3 source code")
5 | (libraries core menhirLib iec_checker.core))
6 |
7 | (menhir
8 | (modules parser)
9 | (flags ("--dump" "--explain")))
10 | ; (flags ("--dump" "--explain" "--trace")))
11 |
12 | (ocamllex
13 | (modules lexer))
14 |
15 | ; Make warnings non-fatal. This is required while parser is WIP.
16 | (env (dev (flags (:standard -warn-error -A))))
17 |
--------------------------------------------------------------------------------
/test/test_zerodiv.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | sys.path.append(os.path.join(os.path.dirname(
5 | os.path.abspath(__file__)), "../src"))
6 | from python.core import run_checker # noqa
7 | from python.dump import DumpManager # noqa
8 |
9 |
10 | def test_zerodiv():
11 | f = 'st/zero-division.st'
12 | fdump = f'{f}.dump.json'
13 | checker_warnings, rc = run_checker([f])
14 | assert rc == 0
15 | checker_warnings.count('ZeroDivision') == 2
16 | with DumpManager(fdump):
17 | pass
18 |
--------------------------------------------------------------------------------
/src/analysis/cyclomatic_complexity.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 |
6 | let eval_mccabe cfg =
7 | (* The McCabe complexity is calculated by:
8 | M = E - N + 2P
9 | Where E is the number of edges in the CFG, N is the number of nodes
10 | and P is the number of disconnected parts of the graph. *)
11 | let e = Cfg.get_number_of_edges cfg
12 | and n = List.length (Cfg.get_reachable_ids cfg) in
13 | let p = n - (List.length (Cfg.get_all_ids cfg)) in
14 | (e - n + 2 * p)
15 |
--------------------------------------------------------------------------------
/src/core/sel.mli:
--------------------------------------------------------------------------------
1 | (** Module to reconstruct IEC61131-3 source code from the SEL XML files.
2 | See: https://github.com/jubnzv/iec-checker/issues/6 for the description of
3 | this format. *)
4 |
5 | val reconstruct_from_channel_opt : in_channel -> string option
6 | (** [reconstruct_from_channel channel] Tries to reconstruct the source code
7 | from the input [channel]. If the content of the given channel contains the
8 | valid IEC61131-3 source code, this function returns it. Otherwise it returns
9 | None. *)
10 |
11 |
--------------------------------------------------------------------------------
/test/selxml/Project Info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 32
8 | 3530
9 | SEL_RTAC
10 |
11 |
12 |
13 | POUs
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp8.st:
--------------------------------------------------------------------------------
1 | PROGRAM demo
2 | VAR
3 | x : REAL;
4 | END_VAR
5 |
6 | IF x = 0.0 THEN (* PLCOPEN CP-8 *)
7 | x := x + 1;
8 | END_IF;
9 |
10 | IF x <> 0.0 THEN (* PLCOPEN CP-8 *)
11 | x := x + 1;
12 | END_IF;
13 |
14 | IF 0.0 = x THEN (* PLCOPEN CP-8 *)
15 | x := x + 1;
16 | END_IF;
17 |
18 | IF 0.0 <> x THEN (* PLCOPEN CP-8 *)
19 | x := x + 1;
20 | END_IF;
21 |
22 | IF REAL_TO_INT(x) <> 0 THEN
23 | x := x + 1;
24 | END_IF;
25 | END_PROGRAM
26 |
27 |
--------------------------------------------------------------------------------
/test/st/good/multiple-pous.st:
--------------------------------------------------------------------------------
1 | FUNCTION function0 : BOOL
2 | VAR
3 | LocalVar0 : DINT;
4 | LocalVar0_ : DINT;
5 | END_VAR
6 | LocalVar0 := LocalVar0 / 2;
7 | END_FUNCTION
8 |
9 | PROGRAM program0
10 | VAR_ACCESS
11 | acc : Var1 : DINT;
12 | acc : Var2 : DINT;
13 | END_VAR
14 | Var1 := 19 / 0;
15 | Var2 := Var1 / 1;
16 | Var2 := Var2 / 0;
17 | END_PROGRAM
18 |
19 | FUNCTION function1 : BOOL
20 | VAR
21 | LocalVar0 : DINT;
22 | LocalVar0_ : DINT;
23 | END_VAR
24 | LocalVar0 := LocalVar0 / 2;
25 | END_FUNCTION
26 |
27 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp28.st:
--------------------------------------------------------------------------------
1 | PROGRAM demo
2 | VAR
3 | x : TIME;
4 | END_VAR
5 |
6 | IF x = T#100MS THEN (* PLCOPEN CP-28 *)
7 | x := x + 1;
8 | END_IF;
9 |
10 | IF x <> T#100MS THEN (* PLCOPEN CP-28 *)
11 | x := x + 1;
12 | END_IF;
13 |
14 | IF T#100MS = x THEN (* PLCOPEN CP-28 *)
15 | x := x + 1;
16 | END_IF;
17 |
18 | IF T#100MS <> x THEN (* PLCOPEN CP-28 *)
19 | x := x + 1;
20 | END_IF;
21 |
22 | IF REAL_TO_INT(x) <> 0 THEN
23 | x := x + 1;
24 | END_IF;
25 | END_PROGRAM
26 |
27 |
--------------------------------------------------------------------------------
/src/core/warn.mli:
--------------------------------------------------------------------------------
1 | (** Warning generated by static analazyer. *)
2 | type warn_ty =
3 | | Inspection
4 | | InternalError
5 | [@@deriving yojson]
6 |
7 | type t = {
8 | linenr: int;
9 | column: int;
10 | file: string;
11 | id: string;
12 | msg: string;
13 | ty: warn_ty [@key "type"];
14 | } [@@deriving yojson]
15 |
16 | val mk : ?ty:(warn_ty) -> ?file:(string) -> int -> int -> string -> string -> t
17 | val mk_internal : ?id:(string) -> string -> t
18 | val mk_from_lexbuf : Lexing.lexbuf -> string -> string -> t
19 |
20 | val to_string : t -> string
21 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/System/Main Controller.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 |
9 | 100
10 | 15000
11 |
12 |
13 | Automation
14 | 1000
15 | 15000
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/ProjSpace_Minimal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 | ProjSpace_Minimal
9 | Program
10 |
11 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/st/good/function-declaration.st:
--------------------------------------------------------------------------------
1 | (* Test for declaration of Function POU. *)
2 | FUNCTION fn1 : INT
3 | VAR_INPUT
4 | vi1 : INT;
5 | vi2 : DINT;
6 | END_VAR
7 | VAR_OUTPUT
8 | vo1 : INT;
9 | END_VAR
10 | VAR_IN_OUT
11 | vio1 : INT;
12 | END_VAR
13 | VAR_IN_OUT
14 | vio1 : INT;
15 | END_VAR
16 | VAR
17 | v1 : INT;
18 | END_VAR
19 | VAR_EXTERNAL
20 | ve1 : INT;
21 | END_VAR
22 | VAR_TEMP
23 | vt1 : INT;
24 | END_VAR
25 |
26 | vio1 := 42;
27 | END_FUNCTION
28 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp6.st:
--------------------------------------------------------------------------------
1 | FUNCTION demo : INT
2 | VAR_EXTERNAL
3 | x1 : INT; (* PLCOPEN-CP6 *)
4 | END_VAR
5 | x1 := 42;
6 | END_FUNCTION
7 |
8 | FUNCTION_BLOCK demo
9 | VAR_EXTERNAL
10 | x1 : INT; (* PLCOPEN-CP6 *)
11 | END_VAR
12 | x1 := 42;
13 | END_FUNCTION_BLOCK
14 |
15 | PROGRAM demo
16 | VAR_EXTERNAL
17 | x1 : INT;
18 | END_VAR
19 | x1 := 42;
20 | END_PROGRAM
21 |
22 | CLASS test1
23 | VAR_EXTERNAL
24 | x1 : INT;
25 | END_VAR
26 |
27 | METHOD PRIVATE count
28 | x1 := 42;
29 | END_METHOD
30 |
31 | END_CLASS
32 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/ProjSpace_Example.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 | ProjSpace_Example
9 | FunctionBlock
10 |
11 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/selxml/POUs/POUs Space/POUSpace_GVL.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | POUSpace_GVL
4 | GVL
5 |
8 |
9 |
10 | ffbfa93a-b94d-45fc-a329-229860183b1d
11 | ]]>
12 |
13 |
--------------------------------------------------------------------------------
/src/core/dump.mli:
--------------------------------------------------------------------------------
1 | module S = Syntax
2 |
3 | (** IEC program scheme used in yojson serialization. *)
4 | type dump_scheme = {
5 | version: string; (** Scheme version *)
6 | functions: S.function_decl list;
7 | function_blocks: S.fb_decl list;
8 | programs: S.program_decl list;
9 | configurations: S.configuration_decl list;
10 | types: S.derived_ty_decl list;
11 | environments: Env.t list;
12 | cfgs: Cfg.t list;
13 | } [@@deriving to_yojson]
14 |
15 | val create_dump : dst_file:string -> S.iec_library_element list -> Env.t list -> Cfg.t list -> unit
16 | (** [create_dump] Save input AST in a JSON file. *)
17 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp13.st:
--------------------------------------------------------------------------------
1 | (* Uncompatible: Recursive functions are forbidden *)
2 | FUNCTION Factorial : INT
3 | VAR_INPUT
4 | X : INT;
5 | END_VAR
6 |
7 | IF X > 1 THEN
8 | Factorial := Factorial(X - 1) * X;
9 | ELSE
10 | Factorial := X;
11 | END_IF;
12 | END_FUNCTION
13 |
14 | (* Compatible iterative implementation *)
15 | FUNCTION Factorial_good : INT
16 | VAR_INPUT
17 | X : INT;
18 | END_VAR
19 | VAR
20 | Acc : INT;
21 | END_VAR
22 |
23 | FOR I := 0 TO 100 DO
24 | Acc := Acc * X;
25 | END_FOR;
26 | Factorial := Acc;
27 | END_FUNCTION
28 |
--------------------------------------------------------------------------------
/test/st/good/direct-variables.st:
--------------------------------------------------------------------------------
1 | (* Examples of directly represented variables from ch. 6.5.5 *)
2 | PROGRAM simple
3 | VAR
4 | v01 AT %IW215 : DINT;
5 | v02 AT %QB7 : DINT;
6 | v03 AT %MD48 : DINT;
7 | v04 AT %IX1 : DINT;
8 | v05 AT %I1 : DINT;
9 | v06 AT %IB2 : DINT;
10 | v07 AT %IW3 : DINT;
11 | v08 AT %ID4 : DINT;
12 | v09 AT %IL5 : DINT;
13 | v10 AT %IB0 : DINT;
14 | v11 AT %QX7.5 : DINT;
15 | v12 AT %MW1.7.9 : DINT;
16 | v13 AT %M* : DINT;
17 | v14 AT %MW10.2.4.1 : INT;
18 | v15 AT %MW11 : INT;
19 | v16 AT %MW12: INT;
20 | END_VAR
21 |
22 | v01 := 92;
23 | END_PROGRAM
24 |
--------------------------------------------------------------------------------
/test/test_plcopen_xml.py:
--------------------------------------------------------------------------------
1 | """Tests PLCOpen XML parser."""
2 | import sys
3 | import os
4 | import pytest
5 |
6 | sys.path.append(os.path.join(os.path.dirname(
7 | os.path.abspath(__file__)), "../src"))
8 | from python.core import run_checker # noqa
9 | from python.dump import DumpManager # noqa
10 |
11 |
12 | @pytest.mark.skip(reason="TODO")
13 | def test_no_parser_errors():
14 | f = os.path.join('./test/plcopen/example.xml')
15 | fdump = f'{f}.dump.json'
16 | checker_warnings, rc = run_checker([f], '-i', 'xml')
17 | assert rc == 0, f"Incorrect exit code for {f}"
18 | with DumpManager(fdump):
19 | pass
20 |
--------------------------------------------------------------------------------
/src/core/tok_info.ml:
--------------------------------------------------------------------------------
1 | open Core
2 |
3 | type t = { id : int; linenr : int; col : int } [@@deriving yojson, show]
4 |
5 | let next_id =
6 | let n = ref (-1) in
7 | fun () ->
8 | incr n;
9 | !n
10 |
11 | let create lexbuf =
12 | let id = next_id () in
13 | let linenr = lexbuf.Lexing.lex_curr_p.pos_lnum in
14 | let col = lexbuf.Lexing.lex_curr_p.pos_cnum - lexbuf.Lexing.lex_curr_p.pos_bol in
15 | { id; linenr; col }
16 |
17 | let create_dummy () =
18 | let id = next_id () in
19 | let linenr = -1 in
20 | let col = -1 in
21 | { id; linenr; col }
22 |
23 | let to_string ti =
24 | Printf.sprintf "%d:%d" ti.linenr ti.col
25 |
--------------------------------------------------------------------------------
/src/core/warn_output.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module W = Warn
3 |
4 | type output_format =
5 | | Plain
6 | | Json
7 |
8 | let print_report warnings fmt =
9 | match fmt with
10 | | Plain -> begin
11 | List.fold_left warnings
12 | ~f:(fun acc w -> acc @ [W.to_string w])
13 | ~init:[]
14 | |> String.concat ~sep:"\n"
15 | |> Printf.printf "%s\n"
16 | end
17 | | Json -> begin
18 | let json_list =
19 | List.fold_left warnings
20 | ~f:(fun out w -> (W.to_yojson w) :: out)
21 | ~init:[]
22 | in
23 | Yojson.Safe.to_string (`List json_list)
24 | |> Printf.printf "%s\n"
25 | end
26 |
--------------------------------------------------------------------------------
/test/st/good/case-insensitive.st:
--------------------------------------------------------------------------------
1 | (* Keywords and identifiers are case-insensitive *)
2 | Program simple
3 | VaR
4 | v01 : DiNt;
5 | eND_var
6 |
7 | v01 := 92;
8 | V01 := 92;
9 | END_PROGRAM
10 |
11 | PRoGRAM square_root
12 | var
13 | a, b, c, d : REAL;
14 | x1, x2 : REaL;
15 | nroots : InT;
16 | END_VAR
17 |
18 | d := b*b - 4.0*a;
19 | d := b*b - 4.0*a*c;
20 | IF d < 0.0
21 | THEN nroots := 0;
22 | ELSIF d = 0.0
23 | THEN
24 | nroots := 1;
25 | X1 := -b/(2.0*A);
26 | X1 := -b;
27 | ELSE
28 | nroots := 2;
29 | X1 := (-b + SQRT(d))/(2.0*a);
30 | X2 := (-b - SQRT(d))/(2.0*a);
31 | END_IF;
32 |
33 | ENd_ProGRAM
34 |
--------------------------------------------------------------------------------
/test/st/good/simple.st:
--------------------------------------------------------------------------------
1 | FUNCTION_BLOCK Example
2 | VAR_EXTERNAL
3 | global_var : INT;
4 | END_VAR
5 | global_var := 42;
6 | END_FUNCTION_BLOCK
7 |
8 | PROGRAM Simple
9 | VAR_INPUT
10 | x : TIME;
11 | END_VAR
12 | VAR
13 | temp : DINT;
14 | i : DINT;
15 | arr1: ARRAY [1..2] OF BOOL;
16 | unused_var AT %IW1.2 : REAL := 200.0;
17 | head AT %B0 : INT;
18 | END_VAR
19 |
20 | WHILE i < 10 DO
21 | IF i = 5 THEN
22 | temp := i;
23 | EXIT;
24 | i := 42;
25 | END_IF
26 | i := i + 1;
27 | END_WHILE
28 |
29 | ARR1[3] := 19;
30 | ARR1[2,1] := 19;
31 | IF x = T#100MS THEN
32 | %B0 := 42;
33 | END_IF
34 | END_PROGRAM
35 |
--------------------------------------------------------------------------------
/test/selxml/POUs/POUs Space/POUSpace_Minimal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | POUSpace_Minimal
4 | Program
5 |
10 |
11 |
12 |
13 | 6f9dac99-8de1-4efc-8465-68ac443b7d08
14 | ]]>
15 |
16 |
--------------------------------------------------------------------------------
/test/selxml/POUs/POUs Space/POUSpace_Example.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | POUSpace_Example
4 | FunctionBlock
5 |
10 |
11 |
12 |
13 | 6f9dac99-8de1-4efc-8465-68ac443b7d08
14 | ]]>
15 |
16 |
--------------------------------------------------------------------------------
/test/test_unused_variable.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | sys.path.append(os.path.join(os.path.dirname(
5 | os.path.abspath(__file__)), "../src"))
6 | from python.core import check_program, filter_warns # noqa
7 | from python.dump import DumpManager # noqa
8 |
9 |
10 | def test_unused_local_variable():
11 | fdump = f'stdin.dump.json'
12 | warns, rc = check_program(
13 | """
14 | PROGRAM p
15 | VAR
16 | a : INT;
17 | b : INT;
18 | c : INT;
19 | END_VAR
20 | b := 1 + c;
21 | END_PROGRAM
22 | """.replace('\n', ''))
23 | assert rc == 0
24 | assert len(filter_warns(warns, 'UnusedVariable')) == 1
25 | with DumpManager(fdump) as dm:
26 | scheme = dm.scheme
27 | assert scheme
28 |
--------------------------------------------------------------------------------
/src/lib/plcopen_l17.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = IECCheckerCore.Syntax
3 | module TI = IECCheckerCore.Tok_info
4 | module Warn = IECCheckerCore.Warn
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let check_stmt = function
8 | | S.StmIf (ti, _, _, _, else_exprs) -> (
9 | match else_exprs with
10 | | [] ->
11 | let msg = "Each IF instruction should have an ELSE clause" in
12 | let w = Warn.mk ti.linenr ti.col "PLCOPEN-L17" msg in
13 | Some w
14 | | _ -> None )
15 | | _ -> None
16 |
17 | let do_check elems =
18 | let stmts = AU.get_stmts elems in
19 | List.map stmts ~f:(fun s -> check_stmt s)
20 | |> List.filter ~f:(fun w -> match w with Some _ -> true | None -> false)
21 | |> List.map ~f:(fun w ->
22 | match w with Some w -> w | None -> assert false)
23 |
--------------------------------------------------------------------------------
/src/core/warn.ml:
--------------------------------------------------------------------------------
1 | type warn_ty =
2 | | Inspection
3 | | InternalError
4 | [@@deriving yojson]
5 |
6 | type t = {
7 | linenr: int;
8 | column: int;
9 | file: string;
10 | id: string;
11 | msg: string;
12 | ty: warn_ty [@key "type"];
13 | } [@@deriving yojson]
14 |
15 | let mk ?(ty=Inspection) ?(file="") linenr column id msg = { linenr; column; file; id; msg; ty }
16 | let mk_internal ?(id="InternalError") msg = mk ~ty:InternalError 0 0 id msg
17 | let mk_from_lexbuf (lexbuf : Lexing.lexbuf) id msg =
18 | let pos = lexbuf.lex_curr_p in
19 | mk ~file:(pos.pos_fname) pos.pos_lnum (pos.pos_cnum - pos.pos_bol) id msg
20 |
21 | let to_string w =
22 | match w.ty with
23 | | Inspection -> Printf.sprintf "%d:%d %s: %s" w.linenr w.column w.id w.msg
24 | | InternalError -> Printf.sprintf "%s: %s" w.id w.msg
25 |
26 |
--------------------------------------------------------------------------------
/test/st/dead-code.st:
--------------------------------------------------------------------------------
1 | FUNCTION dead_code_after_return : INT
2 | VAR
3 | counter : INT := 0;
4 | some_var : INT;
5 | END_VAR
6 | counter := counter + 1;
7 | counter := 2 + 2;
8 | RETURN;
9 | some_var := SQRT(16#42); (* UnreachableCode error *)
10 | some_var := 16#42; (* No additional warnings *)
11 | some_var := 19;
12 | END_FUNCTION
13 |
14 | PROGRAM dead_code_in_the_loops
15 | VAR a : INT; i : INT; END_VAR
16 | WHILE i < 10 DO
17 | IF i = 5 THEN
18 | i := i + 1;
19 | EXIT;
20 | i := 19; (* UnreachableCode error *)
21 | i := 42; (* No additional warnings *)
22 | i := 42;
23 | ELSIF i = 6 THEN
24 | CONTINUE;
25 | i := 3; (* UnreachableCode error *)
26 | i := 44; (* No additional warnings *)
27 | i := 19;
28 | END_IF;
29 | i := i + 2;
30 | END_WHILE;
31 | I := 0;
32 | END_PROGRAM
33 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp3.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let do_check elems =
8 | List.fold_left elems ~init:[] ~f:(fun acc elem -> acc @ (AU.get_var_decls elem))
9 | |> List.fold_left
10 | ~init:[]
11 | ~f:(fun acc decl -> begin
12 | match S.VarDecl.get_located_at decl with
13 | | None -> begin
14 | if phys_equal (S.VarDecl.get_was_init decl) false then
15 | let ti = S.VarDecl.get_var_ti decl in
16 | let msg = Printf.sprintf("Variable %s shall be initialized before being used") @@ S.VarDecl.get_var_name decl in
17 | let w = Warn.mk ti.linenr ti.col "PLCOPEN-CP3" msg in
18 | acc @ [w]
19 | else
20 | acc
21 | end
22 | | _ -> acc
23 | end)
24 |
--------------------------------------------------------------------------------
/dune-project:
--------------------------------------------------------------------------------
1 | (lang dune 2.0)
2 | (using menhir 2.0)
3 |
4 | (generate_opam_files true)
5 |
6 | (name iec_checker)
7 | (version 0.0.4)
8 | (authors "Georgiy Komarov")
9 | (license LGPL-3.0-or-later)
10 | (source (github jubnzv/iec-checker))
11 | (maintainers "Georgiy Komarov ")
12 |
13 | (package
14 | (name iec_checker)
15 | (synopsis "Static analysis of IEC 61131-3 programs")
16 | (depends
17 | (ocaml (>= 4.08))
18 | (core :build)
19 | (clap :build)
20 | (menhir (and :build (= 20210929)))
21 | (menhirLib (and :build (= 20210929)))
22 | (ppx_deriving :build)
23 | (ppx_deriving_yojson :build)
24 | (ppx_fields_conv :build)
25 | (ppx_jane :build)
26 | (ppx_variants_conv :build)
27 | (ppxlib :build)
28 | (re :build)
29 | (xmlm :build)
30 | (yojson :build)))
31 |
--------------------------------------------------------------------------------
/test/st/declaration-analysis.st:
--------------------------------------------------------------------------------
1 | (* This demo includes some declaration errors *)
2 | TYPE
3 | (* String types *)
4 | ST0 : STRING[5]; (* OK *)
5 | ST1 : STRING; (* OK *)
6 | ST2 : STRING[5] := 'foo'; (* OK *)
7 | ST3 : STRING := 'platform_dependent'; (* OK *)
8 | ST4 : STRING[5] := "foobar"; (* Error *)
9 |
10 | (* Subrange types *)
11 | ANALOG_DATA1 : INT (-4095 .. 4095); (* OK *)
12 | ANALOG_DATA1 : INT (-4095 .. 4095) := 2000; (* OK *)
13 | ANALOG_DATA1 : INT (-4095 .. 4095) := 4095; (* OK: See 6.4.4.4.1 *)
14 | ANALOG_DATA1 : INT (-4095 .. 4095) := -4095; (* OK: See 6.4.4.4.1 *)
15 | ANALOG_DATA2 : INT (-4095 .. 4095) := 4099; (* Error *)
16 | ANALOG_DATA2 : INT (-4095 .. 4095) := -4096; (* Error *)
17 | END_TYPE
18 |
--------------------------------------------------------------------------------
/src/core/config.ml:
--------------------------------------------------------------------------------
1 | (** Configuration values including platform and implementation dependent options. *)
2 |
3 | (** Maximum size of STRING and WSTRING data types. *)
4 | let max_string_len = 4096
5 |
6 | (** Threshold of McCabe complexity to generate warnings. *)
7 | let mccabe_complexity_threshold = 15
8 |
9 | (** Threshold of maximum number of statements in POU to generate warnings. *)
10 | let statements_num_threshold = 25
11 |
12 | (* {{{ List of the enabled checks *)
13 | let check_plcopen_cp1 = true
14 | let check_plcopen_cp2 = true
15 | let check_plcopen_cp3 = true
16 | let check_plcopen_cp4 = true
17 | let check_plcopen_cp6 = true
18 | let check_plcopen_cp8 = true
19 | let check_plcopen_cp9 = true
20 | let check_plcopen_cp13 = true
21 | let check_plcopen_cp25 = true
22 | let check_plcopen_cp28 = true
23 | let check_plcopen_l10 = true
24 | let check_plcopen_l17 = true
25 | let check_plcopen_n3 = true
26 | (* }}} *)
27 |
--------------------------------------------------------------------------------
/test/st/good/variables-declaration.st:
--------------------------------------------------------------------------------
1 | PROGRAM REF_DEMO
2 | (* Elementary *)
3 | VAR
4 | v : INT;
5 | v : INT := 1;
6 | v : BOOL;
7 | v : BOOL := FALSE;
8 | v : REAL := 10.0;
9 | END_VAR
10 |
11 | (* Direct and partly-located *)
12 | VAR
13 | v AT %IW1.2 : REAL;
14 | v AT %IW1.2 : REAL := 200.0;
15 | END_VAR
16 |
17 | (* Reference variables *)
18 | VAR
19 | myRefInt: REF_TO INT;
20 | END_VAR
21 |
22 | (* Arrays *)
23 | VAR
24 | A : ARRAY[0..5] OF INT;
25 | A : ARRAY[0..5] OF INT := [1,2,3];
26 | aWStrings: ARRAY[0..1] OF WSTRING := ["1234", "5678"];
27 | END_VAR
28 |
29 | (* Empty case *)
30 | VAR
31 | END_VAR
32 |
33 | (* Retain / non-retain variables *)
34 | VAR RETAIN foo : INT; END_VAR
35 | VAR NON_RETAIN bar : INT; END_VAR
36 |
37 | myRefInt := 1;
38 | END_PROGRAM
39 |
40 | PROGRAM no_variables
41 | myRefInt := 1;
42 | END_PROGRAM
43 |
--------------------------------------------------------------------------------
/src/core/config.mli:
--------------------------------------------------------------------------------
1 | (** Configuration values including platform and implementation dependent options. *)
2 |
3 | val max_string_len : int
4 | (** Maximum size of STRING and WSTRING data types. *)
5 |
6 | val mccabe_complexity_threshold : int
7 | (** Threshold of McCabe complexity to generate warnings. *)
8 |
9 | val statements_num_threshold : int
10 | (** Threshold of maximum number of statements in POU to generate warnings. *)
11 |
12 | (* {{{ List of the enabled checks *)
13 | val check_plcopen_cp1 : bool
14 | val check_plcopen_cp2 : bool
15 | val check_plcopen_cp3 : bool
16 | val check_plcopen_cp4 : bool
17 | val check_plcopen_cp6 : bool
18 | val check_plcopen_cp8 : bool
19 | val check_plcopen_cp9 : bool
20 | val check_plcopen_cp13 : bool
21 | val check_plcopen_cp25 : bool
22 | val check_plcopen_cp28 : bool
23 | val check_plcopen_l10 : bool
24 | val check_plcopen_l17 : bool
25 | val check_plcopen_n3 : bool
26 | (* }}} *)
27 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/ProjSpace_Simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 | ProjSpace_Simple
9 | Program
10 |
11 |
22 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/st/good/user1.st:
--------------------------------------------------------------------------------
1 | (* a simple ST program that call a user defined function, and executes a FOR loop *)
2 | PROGRAM prog1
3 |
4 | VAR_TEMP RETAIN
5 |
6 | I, Z : INT ;
7 | INITIAL : BOOL := FALSE;
8 | X1 : INT := 10;
9 | Y1 : INT := 20;
10 | R1 : REAL := 3.0;
11 | A : BOOL := TRUE;
12 | DI1 : BOOL := FALSE;
13 |
14 | I_I1 AT %MW10.2.4.1 : INT;
15 | I_I2 AT %MW11 : INT;
16 | O_I1 AT %QW63.1 : INT;
17 |
18 | END_VAR
19 |
20 | (* currently we allow GLOBAL blocks inside PROGRAMS *)
21 | VAR_GLOBAL
22 | G1: BOOL;
23 | END_VAR
24 |
25 | IF INITIAL = FALSE THEN
26 | I_I2 := 100;
27 | INITIAL := TRUE;
28 | END_IF;
29 |
30 | Z := func1(X1, Y1); // user defined function
31 |
32 | A := A AND G1;
33 |
34 | G1 := NOT G1; // changes in each run
35 |
36 | Y1 := Z + 5;
37 |
38 | FOR I := 1 TO 3 DO
39 | R1 := SQRT(R1);
40 | X1 := I;
41 | END_FOR;
42 |
43 | Y1 := Z + 1;
44 | O_I1 := Y1;
45 |
46 |
47 | END_PROGRAM
48 |
--------------------------------------------------------------------------------
/test/st/good/multiple-variables.st:
--------------------------------------------------------------------------------
1 | PROGRAM program0
2 | VAR_ACCESS
3 | dummy : va1 : DINT;
4 | dummy : va2 : DINT;
5 | dummy : va3 : DINT;
6 | END_VAR
7 | VAR_INPUT
8 | vi1, vi2 : DINT;
9 | vi3, vi4, vi5 : DINT;
10 | vi6 : DINT;
11 | END_VAR
12 | VAR_OUTPUT
13 | vo1, vo2 : DINT;
14 | vo3, vo4, vo5 : DINT;
15 | vo6 : DINT;
16 | END_VAR
17 | VAR_IN_OUT
18 | vio1, vio2 : DINT;
19 | vio3, vio4, vio5 : DINT;
20 | vio6 : DINT;
21 | END_VAR
22 | VAR
23 | v1, v2 : DINT;
24 | v3, v4, v5 : DINT;
25 | v6 : DINT;
26 | END_VAR
27 | VAR_EXTERNAL
28 | ve1 : DINT;
29 | ve2 : DINT;
30 | ve3 : DINT;
31 | END_VAR
32 | VAR_TEMP
33 | vt1 : DINT;
34 | vt2 : DINT;
35 | vt3 : DINT;
36 | END_VAR
37 | VAR
38 | vinc1 AT %Q* : INT;
39 | vinc2 AT %I* : INT;
40 | vinc3 AT %M* : INT;
41 | END_VAR
42 | VAR
43 | v1 : DINT;
44 | v2 AT %M* : INT;
45 | END_VAR
46 |
47 | vi1 := vi1 / 1;
48 | END_PROGRAM
49 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp6.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 |
8 | let check_elem elem =
9 | match elem with
10 | | S.IECFunction _ | S.IECFunctionBlock _ | S.IECClass _ -> begin
11 | AU.get_var_decls elem
12 | |> List.fold_left
13 | ~init:[]
14 | ~f:(fun acc var_decl -> begin
15 | match S.VarDecl.get_attr var_decl with
16 | | Some S.VarDecl.VarExternal _ -> begin
17 | let ti = S.VarDecl.get_var_ti var_decl
18 | and msg = "External variables in functions, function blocks and classes should be avoided"
19 | in
20 | acc @ [(Warn.mk ti.linenr ti.col "PLCOPEN-CP6" msg)]
21 | end
22 | | _ -> acc
23 | end)
24 | end
25 | | S.IECProgram _ | S.IECConfiguration _ | S.IECType _ | S.IECInterface _ -> []
26 |
27 | let do_check elems =
28 | List.fold_left elems ~init:[] ~f:(fun acc elem -> acc @ (check_elem elem))
29 |
--------------------------------------------------------------------------------
/test/st/good/generic-types.st:
--------------------------------------------------------------------------------
1 | (* Extended example from Beckhoff: https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/18014401038986379.html&id= *)
2 | FUNCTION F_AnyBitFunc : BOOL
3 | VAR_INPUT
4 | value : ANY_BIT;
5 | END_VAR
6 | value := 0;
7 | END_FUNCTION
8 |
9 | FUNCTION F_AnyDateFunc : BOOL
10 | VAR_INPUT
11 | value : ANY_DATE;
12 | END_VAR
13 | value := 0;
14 | END_FUNCTION
15 |
16 | FUNCTION F_AnyFunc : BOOL
17 | VAR_INPUT
18 | value : ANY;
19 | END_VAR
20 | value := 0;
21 | END_FUNCTION
22 |
23 | FUNCTION F_AnyIntFunc : BOOL
24 | VAR_INPUT
25 | value : ANY_INT;
26 | END_VAR
27 | value := 0;
28 | END_FUNCTION
29 |
30 | FUNCTION F_AnyNumFunc : BOOL
31 | VAR_INPUT
32 | value : ANY_NUM;
33 | END_VAR
34 | value := 0;
35 | END_FUNCTION
36 |
37 | FUNCTION F_AnyRealFunc : BOOL
38 | VAR_INPUT
39 | value : ANY_REAL;
40 | END_VAR
41 | value := 0;
42 | END_FUNCTION
43 |
44 | FUNCTION F_AnyStringFunc : BOOL
45 | VAR_INPUT
46 | value : ANY_STRING;
47 | END_VAR
48 | value := 0;
49 | END_FUNCTION
50 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp9.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 | open IECCheckerAnalysis
4 |
5 | module AU = Ast_util
6 | module S = Syntax
7 | module CC = Cyclomatic_complexity
8 |
9 | let get_mccabe_violations cfg =
10 | let cc = CC.eval_mccabe cfg in
11 | if cc > Config.mccabe_complexity_threshold then
12 | let msg = Printf.sprintf "Code is too complex (%d McCabe complexity)" cc in
13 | let w = Warn.mk 0 0 "PLCOPEN-CP9" msg in
14 | [w]
15 | else []
16 |
17 | let get_statements_num_violations elem =
18 | let stmts_num = AU.get_stmts_num elem in
19 | if stmts_num > Config.statements_num_threshold then
20 | let msg = Printf.sprintf "Code is too complex (%d statements)" stmts_num in
21 | let w = Warn.mk 0 0 "PLCOPEN-CP9" msg in
22 | [w]
23 | else []
24 |
25 | let do_check elems cfgs =
26 | List.fold_left
27 | cfgs
28 | ~init:[]
29 | ~f:(fun acc cfg -> acc @ (get_mccabe_violations cfg))
30 | |> List.append
31 | @@ List.fold_left
32 | elems
33 | ~init:[]
34 | ~f:(fun acc elem -> acc @ (get_statements_num_violations elem))
35 |
36 |
--------------------------------------------------------------------------------
/test/selxml/POUs/POUs Space/POUSpace_Simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | POUSpace_Simple
4 | Program
5 |
17 |
31 |
32 |
33 | 6f9dac99-8de1-4efc-8465-68ac443b7d08
34 | ]]>
35 |
36 |
--------------------------------------------------------------------------------
/test/test_core.py:
--------------------------------------------------------------------------------
1 | """Tests for internal errors."""
2 | import sys
3 | import os
4 |
5 | sys.path.append(os.path.join(os.path.dirname(
6 | os.path.abspath(__file__)), "../src"))
7 | from python.core import run_checker # noqa
8 |
9 |
10 | def test_missing_file():
11 | f = 'st/foo.bar'
12 | checker_warnings, rc = run_checker([f])
13 | assert rc == 1
14 | assert len(checker_warnings) == 1
15 | cv = checker_warnings[0]
16 | assert cv.id == 'FileNotFoundError'
17 |
18 |
19 | def test_large_file():
20 | """Make sure that there are no stack overflows on large programs."""
21 | fname = 'st/_TEMP_large.st'
22 | with open(fname, 'w') as f:
23 | f.write(f"""
24 | PROGRAM test_for
25 | VAR a : INT; i : INT; END_VAR
26 | FOR i := 0 TO 10 BY 2 DO
27 | """)
28 | for _ in range(1000):
29 | f.write('a := a + i;\n')
30 | f.write(f"""
31 | END_FOR;
32 | i := 0;
33 | END_PROGRAM
34 | """)
35 | checker_warnings, rc = run_checker([fname])
36 | os.remove(fname)
37 |
--------------------------------------------------------------------------------
/.github/workflows/unit_tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-18.04
9 | strategy:
10 | matrix:
11 | python-version: [ '3.8' ]
12 | ocaml-compiler: [ '4.12.x' ]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v1
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Setup OCaml ${{ matrix.ocaml-compiler }}
21 | uses: avsm/setup-ocaml@v2
22 | with:
23 | ocaml-compiler: ${{ matrix.ocaml-compiler }}
24 | - name: Build OCaml core
25 | run: |
26 | opam install --deps-only .
27 | eval $(opam env)
28 | make build
29 | - name: Install Python dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
33 | - name: Test with pytest
34 | run: |
35 | eval $(opam env)
36 | make test
37 |
--------------------------------------------------------------------------------
/iec_checker.opam:
--------------------------------------------------------------------------------
1 | # This file is generated by dune, edit dune-project instead
2 | opam-version: "2.0"
3 | version: "0.0.4"
4 | synopsis: "Static analysis of IEC 61131-3 programs"
5 | maintainer: ["Georgiy Komarov "]
6 | authors: ["Georgiy Komarov"]
7 | license: "LGPL-3.0-or-later"
8 | homepage: "https://github.com/jubnzv/iec-checker"
9 | bug-reports: "https://github.com/jubnzv/iec-checker/issues"
10 | depends: [
11 | "dune" {>= "2.0"}
12 | "ocaml" {>= "4.08"}
13 | "core" {build}
14 | "clap" {build}
15 | "menhir" {build & = "20210929"}
16 | "menhirLib" {build & = "20210929"}
17 | "ppx_deriving" {build}
18 | "ppx_deriving_yojson" {build}
19 | "ppx_fields_conv" {build}
20 | "ppx_jane" {build}
21 | "ppx_variants_conv" {build}
22 | "ppxlib" {build}
23 | "re" {build}
24 | "xmlm" {build}
25 | "yojson" {build}
26 | ]
27 | build: [
28 | ["dune" "subst"] {pinned}
29 | [
30 | "dune"
31 | "build"
32 | "-p"
33 | name
34 | "-j"
35 | jobs
36 | "@install"
37 | "@runtest" {with-test}
38 | "@doc" {with-doc}
39 | ]
40 | ]
41 | dev-repo: "git+https://github.com/jubnzv/iec-checker.git"
42 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp8.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let is_float = function
8 | | S.ExprConstant (_, c) -> begin
9 | match c with
10 | | S.CReal _ -> true
11 | | _ -> false
12 | end
13 | | _ -> false
14 |
15 | let check_elem elem =
16 | AU.get_pou_exprs elem
17 | |> List.fold_left ~init:[]
18 | ~f:(fun acc expr -> begin
19 | match expr with
20 | | S.ExprBin(ti, lhs, operator, rhs) -> begin
21 | match operator with
22 | | NEG | EQ -> begin
23 | if (is_float lhs) || (is_float rhs) then begin
24 | let msg = "Floating point comparison shall not be equality or inequality" in
25 | acc @ [(Warn.mk ti.linenr ti.col "PLCOPEN-CP8" msg)]
26 | end
27 | else acc
28 | end
29 | | _ -> acc
30 | end
31 | | _ -> acc
32 | end)
33 |
34 | let do_check elems =
35 | List.fold_left
36 | ~init:[]
37 | elems
38 | ~f:(fun acc elem -> acc @ (check_elem elem))
39 |
--------------------------------------------------------------------------------
/test/st/good/configurations.st:
--------------------------------------------------------------------------------
1 | (* Demo program with all possible TASK entries.
2 | Created with Beremiz IDE (https://beremiz.org/). *)
3 | FUNCTION function0 : BOOL
4 | VAR
5 | LocalVar0 : DINT;
6 | END_VAR
7 |
8 | LocalVar0 := 42;
9 | END_FUNCTION
10 |
11 | PROGRAM program0
12 | VAR
13 | LocalVar0 : DINT;
14 | LocalVar1 : TOD;
15 | END_VAR
16 |
17 | LocalVar0 := 42;
18 | END_PROGRAM
19 |
20 | PROGRAM program1
21 | VAR
22 | LocalVar0 : DINT;
23 | END_VAR
24 |
25 | LocalVar0 := 16#42;
26 | END_PROGRAM
27 |
28 | CONFIGURATION config
29 | VAR_GLOBAL
30 | ConfVar0 : DINT;
31 | ConfVar1 : DINT;
32 | ConfVar2 : DINT;
33 | END_VAR
34 |
35 | RESOURCE resource1 ON PLC
36 | VAR_GLOBAL
37 | ResVar0 : DINT;
38 | ResVar1 : BOOL;
39 | END_VAR
40 | TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
41 | TASK task1(SINGLE := ResVar1,PRIORITY := 0);
42 | TASK task2(INTERVAL := T#2h0m0s3ms,PRIORITY := 1);
43 | PROGRAM instance0 WITH task0 : program0;
44 | PROGRAM instance1 WITH task1 : program1;
45 | PROGRAM instance2 WITH task2 : program1;
46 | END_RESOURCE
47 | END_CONFIGURATION
48 |
--------------------------------------------------------------------------------
/test/test_merge_files.py:
--------------------------------------------------------------------------------
1 | """Test if the checker merges multiple input files correctly."""
2 | import sys
3 | import os
4 | import pytest
5 |
6 | sys.path.append(os.path.join(os.path.dirname(
7 | os.path.abspath(__file__)), "../src"))
8 | from python.core import run_checker # noqa
9 | from python.dump import DumpManager # noqa
10 |
11 |
12 | def test_merge_multiple_files():
13 | files = ['st/merge-1.st', 'st/merge-2.st']
14 | fdump = 'merged-input.st.dump.json'
15 | checker_warnings, rc = run_checker(files, args=["-m"])
16 | assert rc == 0
17 | with DumpManager(fdump):
18 | pass
19 |
20 | def test_merge_multiple_files_different_order():
21 | files = ['st/merge-2.st', 'st/merge-1.st']
22 | fdump = 'merged-input.st.dump.json'
23 | checker_warnings, rc = run_checker(files, args=["-m"])
24 | assert rc == 0
25 | with DumpManager(fdump):
26 | pass
27 |
28 | def merge_is_disabled_for_a_single_input_file():
29 | files = ['st/merge-2.st']
30 | fdump = 'merged-input.st.dump.json'
31 | checker_warnings, rc = run_checker(files, args=["-m"])
32 | assert rc == 0
33 | with DumpManager(fdump):
34 | pass
35 |
--------------------------------------------------------------------------------
/test/test_use_define.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | sys.path.append(os.path.join(os.path.dirname(
5 | os.path.abspath(__file__)), "../src"))
6 | from python.core import run_checker, check_program, filter_warns # noqa
7 | from python.dump import DumpManager # noqa
8 |
9 |
10 | def test_use_define_array():
11 | fdump = f'stdin.dump.json'
12 | warns, rc = check_program(
13 | """
14 | PROGRAM test_arr_len
15 | VAR
16 | ARR1: ARRAY [1..2] OF BOOL;
17 | END_VAR
18 | ARR1[0] := 19; (* error *)
19 | ARR1[1] := 19; (* no false positive *)
20 | ARR1[2] := 19; (* no false positive *)
21 | ARR1[3] := 19; (* error *)
22 | ARR1[2,1] := 19; (* error *)
23 | END_PROGRAM
24 | """.replace('\n', ''))
25 | assert rc == 0
26 | assert len(warns) >= 3
27 | ws = filter_warns(warns, 'OutOfBounds')
28 | assert len(ws) == 3
29 | assert "index 0 is out" in ws[0].msg
30 | assert "index 3 is out" in ws[1].msg
31 | assert "addressed to 2 dimension" in ws[2].msg
32 | with DumpManager(fdump) as dm:
33 | scheme = dm.scheme
34 | assert scheme
35 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp28.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let is_time_or_phys = function
8 | | S.ExprConstant (_, c) -> begin
9 | match c with
10 | | S.CTimeValue _ -> true
11 | | _ -> false
12 | end
13 | | _ -> false
14 |
15 |
16 | let check_elem elem =
17 | AU.get_pou_exprs elem
18 | |> List.fold_left ~init:[]
19 | ~f:(fun acc expr -> begin
20 | match expr with
21 | | S.ExprBin(ti, lhs, operator, rhs) -> begin
22 | match operator with
23 | | NEG | EQ -> begin
24 | if (is_time_or_phys lhs) || (is_time_or_phys rhs) then begin
25 | let msg = "Time and physical measures comparissons shall not be equality or inequality" in
26 | acc @ [(Warn.mk ti.linenr ti.col "PLCOPEN-CP28" msg)]
27 | end
28 | else acc
29 | end
30 | | _ -> acc
31 | end
32 | | _ -> acc
33 | end)
34 |
35 | let do_check elems =
36 | List.fold_left
37 | ~init:[]
38 | elems
39 | ~f:(fun acc elem -> acc @ (check_elem elem))
40 |
--------------------------------------------------------------------------------
/src/python/plugins/cfg_plotter.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from pygraphviz import AGraph
3 |
4 | from .. import om # noqa
5 |
6 |
7 | class CFGPlotter:
8 |
9 | def __init__(self, cfgs: List[om.Cfg]):
10 | self.cfgs = cfgs
11 | self.graph = self.generate_graph()
12 |
13 | def generate_graph(self) -> AGraph:
14 | graph = AGraph(directed=True, splines='curved', overlap='vpsc')
15 | for cfg in self.cfgs:
16 | for bb in cfg.basic_blocks:
17 | style = {}
18 | if bb.type == 'BBExit':
19 | style = dict(style='filled', color='#665c54')
20 | if bb.type == 'BBEntry':
21 | style = dict(style='filled', color='#458588')
22 | graph.add_node(
23 | n=bb.id, label=f'bb={bb.id} stmt={bb.stmt_ids}', **style)
24 |
25 | for succ in bb.succs:
26 | graph.add_edge(bb.id, succ)
27 | for pred in bb.preds:
28 | graph.add_edge(pred, bb.id)
29 | return graph
30 |
31 | def save_file(self, filepath: str):
32 | self.graph.layout()
33 | self.graph.draw(filepath)
34 |
--------------------------------------------------------------------------------
/src/core/env.mli:
--------------------------------------------------------------------------------
1 | (**
2 | Representation of the environments for IEC61131-3 languages.
3 |
4 | IEC61131-3 standard has the similar definition of "scope" - a portion of a
5 | language element within which a declaration or label applies (see ch.
6 | 6.5.2.2).
7 |
8 | This implementation keeps environment which map uses of identifiers to their
9 | semantic information.
10 | *)
11 |
12 | module S = Syntax
13 |
14 | type t
15 |
16 | val get_id : t -> int
17 | (** [get_id] Get the unique ID of the POU that this env belongs to. *)
18 |
19 | val empty : t
20 | (** [empty] Create an empty environment *)
21 |
22 | val mk_global : unit -> t
23 | (** [mk_global] Make a new global environment. *)
24 |
25 | val mk : t (** parent environment *) -> int (** POU id *) -> t
26 | (** [mk] Add a new child environment *)
27 |
28 | val add_vdecl : t -> S.VarDecl.t -> t
29 | (** [add_vdecl] Insert variable declaration in [t]. *)
30 |
31 | val get_vdecls : t -> S.VarDecl.t list
32 | (** [get_vdecls] Return variables declared in [t]. *)
33 |
34 | val lookup_vdecl : t -> string -> S.VarDecl.t option
35 | (** [lookup_vdecl] Search for a given identifier name in the given environment. *)
36 |
37 | val to_yojson : t -> Yojson.Safe.t
38 |
--------------------------------------------------------------------------------
/test/st/good/types.st:
--------------------------------------------------------------------------------
1 | (* This file contains variables with valid type definitions. *)
2 | TYPE
3 | (* Character strings *)
4 | S : STRING;
5 | S1 : STRING[5];
6 | WS : WSTRING;
7 | WS1 : WSTRING[5];
8 | WS1 : WSTRING[25] := 'spaces_disallowed';
9 | C : CHAR;
10 | WC : WCHAR;
11 |
12 | (* Subrange types *)
13 | ANALOG_DATA : INT (-4095 .. 4095);
14 |
15 | (* Enum types *)
16 | Traffic_Light: (Red, Amber, Green);
17 | Painting_colors: (Red, Yellow, Blue) := Blue;
18 |
19 | (* Array types *)
20 | BITS: ARRAY [0..7] OF BOOL := [0,1,1,0,0,1,0,0];
21 |
22 | (* Struct types *)
23 | ANALOG_CHANNEL_CONFIGURATION: STRUCT
24 | RANGE: ANALOG_SIGNAL_RANGE;
25 | MIN_SCALE: INT := -4095;
26 | MAX_SCALE: INT := 4095;
27 | END_STRUCT;
28 | Cooler: STRUCT
29 | Temp: INT;
30 | Cooling: TOF;
31 | END_STRUCT;
32 | Com1_data: STRUCT
33 | head AT %B0 : INT;
34 | length AT %B2 : USINT := 26;
35 | flag1 AT %X3.0 : BOOL;
36 | end AT %B25 : BYTE;
37 | END_STRUCT;
38 | END_TYPE
39 |
40 | PROGRAM arr_demo
41 | VAR
42 | myAnalog_16: ARRAY [1..16] OF DINT;
43 | TBT1 : ARRAY [1..2, 1..3] OF INT;
44 | BITS: ARRAY [0..7] OF BOOL := [0,1,1,0,0,1,0,0];
45 | END_VAR
46 | myAnalog_16[0] := 3;
47 | END_PROGRAM
48 |
--------------------------------------------------------------------------------
/test/selxml/Navigator Layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 | -
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp2.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module TI = Tok_info
5 | module AU = Ast_util
6 | module S = Syntax
7 |
8 | (** Generate warning for a given basic block *)
9 | let mk_warn (bb : Cfg.bb) : Warn.t =
10 | let ti = Cfg.bb_get_ti bb in
11 | Warn.mk ti.linenr ti.col "PLCOPEN-CP2" "All code shall be used in the application"
12 |
13 | (** Find basic blocks inside the loop statements that are unreachable after
14 | CONTINUE/EXIT blocks. *)
15 | let find_unreachable_blocks (cfgs : Cfg.t list) : (Warn.t list) =
16 | let check_cfg (cfg : Cfg.t) : (Warn.t list) =
17 | let module IntSet = Set.Make(Int) in
18 |
19 | let reachable_set = IntSet.of_list (Cfg.get_reachable_ids cfg)
20 | and all_set = IntSet.of_list (Cfg.get_all_ids cfg) in
21 | let unreachable_set = IntSet.diff all_set reachable_set in
22 |
23 | Set.fold
24 | unreachable_set
25 | ~init:[]
26 | ~f:(fun acc id -> begin
27 | let bb = Cfg.get_bb_by_id_exn cfg id in
28 | (* Add blocks without previous nodes in CFG. *)
29 | match bb.preds with
30 | | [] -> acc @ [(mk_warn bb)]
31 | | _ -> acc
32 | end)
33 | in
34 | List.fold_left
35 | cfgs
36 | ~init:[]
37 | ~f:(fun warns c -> warns @ (check_cfg c))
38 |
39 | let do_check (cfgs : Cfg.t list) : Warn.t list =
40 | (* List.iter cfgs ~f:(fun c -> Printf.printf "%s\n" (Cfg.to_string c)); *)
41 | (find_unreachable_blocks cfgs)
42 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp13.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = IECCheckerCore.Syntax
3 | module TI = IECCheckerCore.Tok_info
4 | module AU = IECCheckerCore.Ast_util
5 | module Warn = IECCheckerCore.Warn
6 |
7 | let check_stmt func_name = function
8 | | S.StmFuncCall (ti, f, _) ->
9 | if String.equal (S.Function.get_name f) func_name then
10 | let msg = "POUs shall not call themselves directly or indirectly" in
11 | let w = Warn.mk ti.linenr ti.col "PLCOPEN-CP13" msg in
12 | Some w
13 | else None
14 | | _ -> None
15 |
16 | let check_func_stmts func =
17 | let name =
18 | match func with
19 | | S.IECFunction (_, fd) -> S.Function.get_name fd.id
20 | | S.IECFunctionBlock (_, fbd) -> S.FunctionBlock.get_name fbd.id
21 | | _ -> assert false
22 | in
23 | AU.get_pou_stmts func
24 | |> List.fold_left
25 | ~f:(fun warns stmt ->
26 | let found_warn = check_stmt name stmt in
27 | found_warn :: warns)
28 | ~init:[]
29 |
30 | let do_check elems =
31 | let functions =
32 | List.filter
33 | ~f:(fun e ->
34 | match e with
35 | | S.IECFunction _ | S.IECFunctionBlock _ -> true
36 | | _ -> false)
37 | elems
38 | in
39 | List.fold_left functions
40 | ~f:(fun warns f ->
41 | let found_warns = check_func_stmts f in
42 | List.append warns found_warns)
43 | ~init:[]
44 | |> List.filter ~f:(fun w -> match w with Some _ -> true | None -> false)
45 | |> List.map ~f:(fun w ->
46 | match w with Some w -> w | None -> assert false)
47 |
--------------------------------------------------------------------------------
/test/test_declaration_analysis.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | sys.path.append(os.path.join(os.path.dirname(
5 | os.path.abspath(__file__)), "../src"))
6 | from python.core import run_checker, check_program, filter_warns # noqa
7 | from python.dump import DumpManager # noqa
8 |
9 |
10 | def test_initialization_literal():
11 | f = 'st/declaration-analysis.st'
12 | fdump = f'{f}.dump.json'
13 | checker_warnings, rc = run_checker([f])
14 | assert rc == 0
15 | assert len(checker_warnings) == 3
16 | cv = checker_warnings[0]
17 | assert cv.id == 'OutOfBounds'
18 | # assert cv.linenr == 8
19 | # assert cv.column == 31
20 | with DumpManager(fdump):
21 | pass
22 |
23 |
24 | def test_array_initialized_list():
25 | fdump = f'stdin.dump.json'
26 | warns, rc = check_program(
27 | """
28 | TYPE BITS: ARRAY [1..2, 1..3] OF BOOL := [0,0,0,0,0,0,1,1,1]; END_TYPE
29 | PROGRAM test_p
30 | VAR
31 | ARR1: ARRAY [1..2, 1..3] OF BOOL := [0,0,0,0,0,0,1,1,1];
32 | END_VAR
33 | ARR1[1] := 19;
34 | END_PROGRAM
35 | """.replace('\n', ''))
36 | assert rc == 0
37 | assert len(warns) >= 2
38 | oob_warns = filter_warns(warns, 'OutOfBounds')
39 | assert len(oob_warns) == 2
40 | for w in oob_warns:
41 | assert w.id == 'OutOfBounds'
42 | assert '3 values will be lost' in w.msg
43 | with DumpManager(fdump) as dm:
44 | scheme = dm.scheme
45 | assert scheme
46 | assert len(scheme.types) == 1
47 |
--------------------------------------------------------------------------------
/src/lib/zerodiv.ml:
--------------------------------------------------------------------------------
1 | (* Demo check: Find division to a zero constant. *)
2 |
3 | open Core
4 | open IECCheckerCore.Common
5 | module S = IECCheckerCore.Syntax
6 | module TI = IECCheckerCore.Tok_info
7 | module AU = IECCheckerCore.Ast_util
8 | module Warn = IECCheckerCore.Warn
9 |
10 | let rec collect_warnings (e: S.expr) : Warn.t list =
11 | match e with
12 | | S.ExprBin (_, e1, op, e2) -> (
13 | match (e1, op, e2) with
14 | | S.ExprBin _, _, _ -> collect_warnings e1
15 | | _, _, S.ExprBin _ -> collect_warnings e2
16 | | S.ExprVariable(_, lhs), S.DIV, S.ExprConstant(_, rhs) ->
17 | if S.c_is_zero rhs then
18 | let name = S.VarUse.get_name lhs in
19 | let ti = S.VarUse.get_ti lhs in
20 | let msg = (Printf.sprintf "Variable %s is divided by zero!" name) in
21 | let w = Warn.mk ti.linenr ti.col "ZeroDivision" msg in
22 | [w]
23 | else
24 | []
25 | | S.ExprConstant(_,lhs), S.DIV, S.ExprConstant(_,rhs) ->
26 | if S.c_is_zero rhs then
27 | let v_str = S.c_get_str_value lhs in
28 | let ti = S.c_get_ti lhs in
29 | let msg = (Printf.sprintf "Constant %s is divided by zero!" v_str) in
30 | let w = Warn.mk ti.linenr ti.col "ZeroDivision" msg in
31 | [w]
32 | else
33 | []
34 | | _ -> [] )
35 | | _ -> []
36 |
37 | let do_check elems =
38 | List.fold_left elems ~init:[] ~f:(fun acc elem -> acc @ (AU.get_pou_exprs elem))
39 | |> List.map ~f:(fun exprs -> collect_warnings exprs)
40 | |> list_flatten
41 |
--------------------------------------------------------------------------------
/test/test_selxml.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | sys.path.append(os.path.join(os.path.dirname(
5 | os.path.abspath(__file__)), "../src"))
6 | from python.core import (run_checker_full_out,
7 | binary_default,
8 | filter_warns) # noqa
9 | from python.dump import DumpManager # noqa
10 |
11 |
12 | def test_sel_rtac():
13 | rc, out = run_checker_full_out(['selxml/SEL_RTAC/'], binary_default, "-v", "-i", "selxml")
14 | assert rc == 0
15 | assert("Parsing selxml/SEL_RTAC/ProjSpace_MAIN_POU.xml" in out)
16 | assert("Parsing selxml/SEL_RTAC/Tag Processor.xml" in out)
17 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Minimal.xml" in out)
18 | assert("Parsing selxml/SEL_RTAC/ProjSpace_GVL1.xml" in out)
19 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Simple.xml" in out)
20 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Example.xml" in out)
21 |
22 |
23 | def test_recurse_dirs():
24 | """Test whether the checker will recursively looking up for files in the
25 | nested directories."""
26 | rc, out = run_checker_full_out(['selxml'], binary_default, "-v", "-i", "selxml")
27 | assert rc == 0
28 | assert("Parsing selxml/SEL_RTAC/ProjSpace_MAIN_POU.xml" in out)
29 | assert("Parsing selxml/SEL_RTAC/Tag Processor.xml" in out)
30 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Minimal.xml" in out)
31 | assert("Parsing selxml/SEL_RTAC/ProjSpace_GVL1.xml" in out)
32 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Simple.xml" in out)
33 | assert("Parsing selxml/SEL_RTAC/ProjSpace_Example.xml" in out)
34 |
--------------------------------------------------------------------------------
/src/core/dump.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = Syntax
3 |
4 | type dump_scheme = {
5 | version: string; (** Scheme version *)
6 | functions: S.function_decl list;
7 | function_blocks: S.fb_decl list;
8 | programs: S.program_decl list;
9 | configurations: S.configuration_decl list;
10 | types: S.derived_ty_decl list;
11 | environments: Env.t list;
12 | cfgs: Cfg.t list;
13 | } [@@deriving to_yojson]
14 |
15 | let create_dump ~dst_file elements environments cfgs =
16 | let version = "0.1" in
17 | let functions =
18 | List.fold_left elements
19 | ~f:(fun acc e -> match e with S.IECFunction (_, f) -> acc @ [f] | _ -> acc)
20 | ~init:[]
21 | in
22 | let function_blocks =
23 | List.fold_left elements
24 | ~f:(fun acc e -> match e with S.IECFunctionBlock (_, fb) -> acc @ [fb] | _ -> acc)
25 | ~init:[]
26 | in
27 | let programs =
28 | List.fold_left elements
29 | ~f:(fun acc e -> match e with S.IECProgram (_, p) -> acc @ [p] | _ -> acc)
30 | ~init:[]
31 | in
32 | let configurations =
33 | List.fold_left elements
34 | ~f:(fun acc e -> match e with S.IECConfiguration (_, c) -> acc @ [c] | _ -> acc)
35 | ~init:[]
36 | in
37 | let types =
38 | List.fold_left elements
39 | ~f:(fun acc e -> match e with S.IECType (_, ty) -> acc @ [ty] | _ -> acc)
40 | ~init:[]
41 | in
42 | let scheme = {
43 | version;
44 | functions;
45 | function_blocks;
46 | programs;
47 | configurations;
48 | types;
49 | environments;
50 | cfgs;
51 | } in
52 | Yojson.Safe.to_file dst_file (dump_scheme_to_yojson scheme)
53 |
--------------------------------------------------------------------------------
/src/core/common.ml:
--------------------------------------------------------------------------------
1 | open Core
2 |
3 | exception InternalError of string
4 |
5 | let ignore v =
6 | let _ = v in ()
7 |
8 | let next_id =
9 | let n = ref (-1) in
10 | fun () ->
11 | incr n;
12 | !n
13 |
14 | let sublist l low high =
15 | List.filteri l ~f:(fun i _ -> i >= low && i < high)
16 |
17 | let rec list_flatten = function
18 | | [] -> []
19 | | [] :: t -> list_flatten t
20 | | (x::y) :: t -> x :: (list_flatten (y::t))
21 |
22 | let head_exn = function
23 | | [] -> raise @@ InternalError "List is empty!\n"
24 | | x::_ -> x
25 |
26 | (** Tail-recursive append to process large lists. *)
27 | let append_tr xs ys = List.rev_append (List.rev xs) ys
28 |
29 | (* {{{ Basic routines to work with monads *)
30 | (* The implementation is based on CS3110 Maybe Monad:
31 | https://www.cs.cornell.edu/courses/cs3110/2019sp/textbook/ads/ex_maybe_monad.html *)
32 | let ( >>| ) = Option.( >>| )
33 | let ( >>= ) = Option.( >>= )
34 |
35 | let unwrap_list = function
36 | | Some l -> l
37 | | None -> []
38 |
39 | let return (x : int) : int option =
40 | Some x
41 |
42 | let return_binary op x y =
43 | return (op x y)
44 |
45 | let upgrade_binary op x y =
46 | x >>= fun a ->
47 | y >>= fun b ->
48 | op a b
49 |
50 | let sum_maybe_list values =
51 | let ( + ) = upgrade_binary (return_binary Caml.( + )) in
52 | let rec aux acc values =
53 | match acc with
54 | | Some _ -> begin
55 | match values with
56 | | [] -> acc
57 | | [x] -> ( + ) acc x
58 | | x :: xs -> aux (( + ) acc x) xs
59 | end
60 | | None -> None
61 | in
62 | aux (Some 0) values
63 | (* }}} *)
64 |
--------------------------------------------------------------------------------
/src/analysis/unused_variable.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module AU = IECCheckerCore.Ast_util
3 | module S = IECCheckerCore.Syntax
4 | module Warn = IECCheckerCore.Warn
5 |
6 | let check_pou elem =
7 | let module StringSet = Set.Make(String) in
8 |
9 | (* Get names of variables declared in POU. *)
10 | let get_decl_var_names () =
11 | AU.get_var_decls elem
12 | |> List.map ~f:(fun vardecl -> S.VarDecl.get_var_name vardecl)
13 | in
14 |
15 | (* Get names of variables used in POU. *)
16 | let get_use_var_names () =
17 | AU.filter_exprs
18 | elem
19 | ~f:(fun expr -> begin
20 | match expr with S.ExprVariable _ -> true | _ -> false
21 | end)
22 | |> List.map
23 | ~f:(fun expr -> begin
24 | match expr with
25 | | S.ExprVariable (_, v) -> (S.VarUse.get_name v)
26 | | _ -> assert false
27 | end)
28 | in
29 |
30 | let decl_set = StringSet.of_list (get_decl_var_names ())
31 | and use_set = StringSet.of_list (get_use_var_names ()) in
32 |
33 | StringSet.diff decl_set use_set
34 | |> Set.fold ~init:[]
35 | ~f:(fun acc var_name -> begin
36 | let ti = AU.get_ti_by_name_exn elem var_name in
37 | let text = Printf.sprintf "Found unused local variable: %s" var_name in
38 | acc @ [Warn.mk ti.linenr ti.col "UnusedVariable" text]
39 | end)
40 |
41 | let run elements =
42 | List.fold_left
43 | elements
44 | ~f:(fun warns e ->
45 | let ws = match e with
46 | | S.IECProgram _ | S.IECFunction _ | S.IECFunctionBlock _ -> check_pou e
47 | | _ -> []
48 | in
49 | warns @ ws)
50 | ~init:[]
51 |
--------------------------------------------------------------------------------
/test/st/plcopen-cp9.st:
--------------------------------------------------------------------------------
1 | (* Example from PLCOpen guidelines, page 60.
2 |
3 | The following Function block CHARCURVE has:
4 | Number of Statements 18
5 | McCabe complexity of 12
6 | Prater complexity of 3,89
7 | Halstead complexity of 44,9
8 | Elshof complexity of 0,14
9 |
10 | NOTE: I'm not sure how they evaluated number of statements and appropriate
11 | complexity metrics. I have 4 additional statements for this program, and it
12 | seems correct for me.
13 | *)
14 |
15 | FUNCTION_BLOCK CHARCURVE
16 | VAR_INPUT
17 | IN:INT;
18 | N:BYTE;
19 | END_VAR
20 | (* VAR_IN_OUT *)
21 | (* P : ARRAY [0 .. 10] OF POINT; *)
22 | (* END_VAR *)
23 | VAR_OUTPUT
24 | OUT:INT;
25 | ERR: BYTE;
26 | END_VAR
27 | VAR
28 | I:INT;
29 | END_VAR
30 |
31 | IF N > 1 AND N < 12 THEN
32 | ERR:=0;
33 | (* IF IN < P[0].X THEN *)
34 | IF IN < a THEN
35 | ERR := 2;
36 | OUT := DINT_TO_INT(a);
37 | (* ELSIF IN > P[N-1].X THEN *)
38 | ELSIF IN > a THEN
39 | ERR := 2;
40 | OUT := DINT_TO_INT(a);
41 | ELSE
42 | FOR I:=1 TO N-1 DO
43 | (* IF P[I-1].X >= P[I].X THEN *)
44 | IF a >= a THEN
45 | ERR:=1;
46 | EXIT;
47 | END_IF;
48 | (* IF IN <= P[I].X THEN *)
49 | IF IN <= a THEN
50 | EXIT;
51 | END_IF;
52 | END_FOR;
53 | IF ERR = 0 THEN
54 | (* OUT := DINT_TO_INT(P[I].Y - (P[I].X-IN) * (P[I].Y-P[I-1].Y) / (P[I].X-P[I-1].X)); *)
55 | OUT := DINT_TO_INT(a - (a - IN) * (a - a) / (a - a));
56 | ELSE
57 | OUT:=0;
58 | END_IF;
59 | END_IF;
60 | ELSE
61 | ERR := 4;
62 | END_IF;
63 | END_FUNCTION_BLOCK
64 |
--------------------------------------------------------------------------------
/test/selxml/POUs/Project Information.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Project Information
4 | ProjectInformation
5 |
6 |
7 |
8 | 00000000-0000-0000-0000-000000000000
9 | 00000000-0000-0000-0000-000000000000
10 | 00000000-0000-0000-0000-000000000000
11 | 00000000-0000-0000-0000-000000000000
12 | 00000000-0000-0000-0000-000000000000
13 | 00000000-0000-0000-0000-000000000000
14 | 00000000-0000-0000-0000-000000000000
15 | 00000000-0000-0000-0000-000000000000
16 |
17 |
18 |
19 | ]]>
20 |
21 |
22 | 085afe48-c5d8-4ea5-ab0d-b35701fa6009
23 | ]]>
24 |
25 |
--------------------------------------------------------------------------------
/src/core/env.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = Syntax
3 | module E = Error
4 |
5 | (** Maps over identifier uses, accessible by identifier name *)
6 | module VarDeclMap = struct
7 | type t = (string, S.VarDecl.t, String.comparator_witness) Map.t
8 |
9 | let empty = Map.empty (module String)
10 |
11 | let lookup m (name : string) = Map.find m name
12 |
13 | let add m name var_decl = Map.set m ~key:name ~data:var_decl
14 |
15 | let to_list m =
16 | Map.to_alist m
17 | |> List.fold_left ~init:[] ~f:(fun vds (_, vd) -> vds @ [ vd ])
18 |
19 | let to_yojson (m : t) : Yojson.Safe.t =
20 | let items = Map.fold m ~init:[] ~f:(fun ~key ~data lst ->
21 | `Assoc [key, S.VarDecl.to_yojson data] :: lst
22 | ) in
23 | `List items
24 | end
25 |
26 | type t = {
27 | parent : t option; (** Parent env *)
28 | id: int; (** Unique id of the POU. See: [Syntax.get_pou_id]. *)
29 | var_decls : VarDeclMap.t; (** Variables declared in this env *)
30 | }
31 |
32 | let get_id t = t.id
33 |
34 | let to_yojson env : Yojson.Safe.t =
35 | let var_decls = env.var_decls in
36 | `Assoc [
37 | "var_decls", VarDeclMap.to_yojson var_decls;
38 | ]
39 |
40 | let empty = { parent = None; id = -1; var_decls = VarDeclMap.empty }
41 |
42 | let mk_global () =
43 | let parent = None in
44 | let id = -1 in
45 | let var_decls = VarDeclMap.empty in
46 | { parent; id; var_decls }
47 |
48 | let mk p id =
49 | let parent = Some p in
50 | let var_decls = VarDeclMap.empty in
51 | { parent; id; var_decls }
52 |
53 | let add_vdecl env vd =
54 | let name = S.VarDecl.get_var_name vd in
55 | let vds = VarDeclMap.add env.var_decls name vd in
56 | { env with var_decls = vds }
57 |
58 | let get_vdecls env = VarDeclMap.to_list env.var_decls
59 |
60 | let lookup_vdecl env name = VarDeclMap.lookup env.var_decls name
61 |
--------------------------------------------------------------------------------
/src/core/ast_util.mli:
--------------------------------------------------------------------------------
1 | (** Helpers to work with AST *)
2 | module S = Syntax
3 | module TI = Tok_info
4 |
5 | val expr_to_stmts : S.expr -> S.statement list
6 | (** [expr_to_stmts expr] Convert [expr] to a list of statements. *)
7 |
8 | val get_var_decls : S.iec_library_element -> S.VarDecl.t list
9 | (** [get_var_decls elem] Get all variable declarations from the given [elem]. *)
10 |
11 | val get_pou_stmts : S.iec_library_element -> S.statement list
12 | (** Recursively get all statements from a given POU *)
13 |
14 | val get_top_stmts : S.iec_library_element -> S.statement list
15 | (** [get_top_stmts pou] Non-recursively get statements from a [pou]. *)
16 |
17 | val get_stmts_num : S.iec_library_element -> int
18 | (** [get_stmts_num elem] Return number of statements from [elem] including
19 | nested ones. *)
20 |
21 | val get_stmts : S.iec_library_element list -> S.statement list
22 | (** Collect all statements from each POU *)
23 |
24 | val get_pou_exprs : S.iec_library_element -> S.expr list
25 | (** Collect the expressions from each statement of the POU *)
26 |
27 | val get_var_uses : S.iec_library_element -> S.VarUse.t list
28 | (** Collect all VarUse from the given POU *)
29 |
30 | val filter_exprs : f:(S.expr -> bool) -> S.iec_library_element -> S.expr list
31 | (** [filter_exprs f elem] Return list of expressions that satisfy the predicate
32 | function [f].*)
33 |
34 | val get_ti_by_name_exn : S.iec_library_element -> string -> TI.t
35 | (** [get_ti_by_name_exn elem name] Get token info for variable declaration by it [name]. *)
36 |
37 | val create_envs : S.iec_library_element list -> Env.t list
38 | (** Create the environments for a given configuration elements *)
39 |
40 | val eval_array_capacity : S.arr_subrange list -> int
41 | (** [eval_array_capacity subranges] Evaluate maximum capacity of the array with
42 | respect to [subranges]. *)
43 |
--------------------------------------------------------------------------------
/src/python/dump.py:
--------------------------------------------------------------------------------
1 | """
2 | Routines to work with dump files generated by the ``iec-checker`` binary.
3 | """
4 | import os
5 | import logging
6 | from dataclasses import dataclass
7 | from typing import List, Optional
8 | import ijson
9 |
10 | from .om import Scheme
11 |
12 |
13 | log = logging.getLogger('plugins')
14 | log.setLevel(logging.DEBUG)
15 |
16 |
17 | @dataclass
18 | class PluginWarning:
19 | """Inspection message generated by the Python plugin."""
20 | msg: str
21 |
22 |
23 | class CheckerError(Exception):
24 | """Internal exception generated by the checker."""
25 |
26 |
27 | class DumpManager:
28 | """Class that incapsulates logic over ObjectModel unmarshalled from the
29 | generated dump files."""
30 |
31 | def __init__(self, dump_path: str):
32 | self.dump_path: str = dump_path
33 | self.scheme: Optional[Scheme] = None
34 |
35 | def __enter__(self):
36 | self.scheme = self.mk_scheme()
37 | if not self.scheme:
38 | raise CheckerError(
39 | f'Can\'t extract dump scheme from {self.dump_path}!')
40 | return self
41 |
42 | def __exit__(self, exc_type, exc_val, exc_tb):
43 | self.remove_dump()
44 |
45 | def run_all_inspections(self) -> List[PluginWarning]:
46 | """Run all inspections implemented as Python plugins."""
47 | return []
48 |
49 | def mk_scheme(self) -> Scheme:
50 | scheme = None
51 | with open(self.dump_path, 'rb') as f:
52 | for item in ijson.items(f, ""):
53 | scheme = Scheme.from_dict(item)
54 | if not scheme:
55 | raise Exception(f"Cannot parse JSON scheme from {self.dump_path}")
56 | return scheme
57 |
58 | def remove_dump(self):
59 | """Remove processed dump file."""
60 | try:
61 | os.remove(self.dump_path)
62 | except OSError as e:
63 | raise CheckerError(f'Can\'t remove {self.dump_path}: {str(e)}')
64 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp1.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let get_located_vars_decls elem =
8 | AU.get_var_decls elem
9 | |> List.fold_left
10 | ~init:[]
11 | ~f:(fun acc var_decl -> begin
12 | match (S.VarDecl.get_located_at var_decl) with
13 | | Some loc -> acc @ [loc]
14 | | None -> acc
15 | end)
16 |
17 | let get_located_values_uses elem =
18 | AU.get_pou_exprs elem
19 | |> List.fold_left ~init:[] ~f:(fun acc expr -> begin
20 | match expr with
21 | | S.ExprBin (_, lhs, operator, _) -> begin
22 | if phys_equal operator S.ASSIGN then
23 | match lhs with
24 | | S.ExprVariable (_, v) -> begin
25 | match S.VarUse.get_loc v with
26 | | S.VarUse.DirVar dirvar -> acc @ [S.DirVar.to_string dirvar]
27 | | S.VarUse.SymVar _ -> acc
28 | end
29 | | _ -> acc
30 | else
31 | acc
32 | end
33 | | _ -> acc
34 | end)
35 |
36 | let check_elem elem =
37 | let decls = get_located_vars_decls elem
38 | and uses = get_located_values_uses elem
39 | in
40 | List.fold_left
41 | uses
42 | ~init:[]
43 | ~f:(fun acc u -> begin
44 | acc @ List.fold_left
45 | decls
46 | ~init:[]
47 | ~f:(fun acc d -> begin
48 | if String.equal (S.DirVar.get_name d) u then
49 | let ti = S.DirVar.get_ti d
50 | and msg = Printf.sprintf "Access to a member %s shall be by name" @@ S.DirVar.get_name d
51 | in
52 | acc @ [Warn.mk ti.linenr ti.col "PLCOPEN-CP1" msg]
53 | else
54 | acc
55 | end)
56 | end)
57 |
58 | let do_check elems =
59 | List.fold_left
60 | elems
61 | ~init:[]
62 | ~f:(fun acc elem -> acc @ (check_elem elem))
63 |
--------------------------------------------------------------------------------
/checker.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import sys
4 | from typing import List
5 |
6 | sys.path.append(os.path.join(os.path.dirname(
7 | os.path.abspath(__file__)), "src"))
8 | from python.core import run_checker # noqa
9 | from python.dump import DumpManager # noqa
10 | from python.plugins.cfg_plotter import CFGPlotter # noqa
11 |
12 |
13 | def main(files: List[str], draw_cfg: str = "",
14 | binary: str = "../output/bin/iec_checker"):
15 | for f in files:
16 | if not os.path.isfile(f):
17 | continue
18 | checker_warnings, rc = run_checker(f, binary)
19 | if rc != 0:
20 | print(f'Report for {f}:')
21 | for w in checker_warnings:
22 | print(f'{w}')
23 | continue
24 |
25 | dump_name = f'{f}.dump.json'
26 | plugins_warnings = []
27 | with DumpManager(dump_name) as dm:
28 | plugins_warnings = dm.run_all_inspections()
29 | if draw_cfg:
30 | cfg_plotter = CFGPlotter(dm.scheme.cfgs)
31 | cfg_plotter.save_file(draw_cfg)
32 |
33 | print(f'Report for {f}:')
34 | if checker_warnings or plugins_warnings:
35 | for w in checker_warnings:
36 | print(f'{w}')
37 | for p in plugins_warnings:
38 | print(f'{w}')
39 | else:
40 | print('No errors found!')
41 |
42 |
43 | if __name__ == '__main__':
44 | parser = argparse.ArgumentParser(
45 | description='Static analysis for IEC 61131-3 programs.')
46 | parser.add_argument("files", nargs='*', help="Path to IEC source files")
47 | parser.add_argument("--draw-cfg", type=str,
48 | help="Save control flow graph image at the selected path")
49 | parser.add_argument("-b", "--binary", default=os.path.join("output", "bin", "iec_checker"),
50 | help="File path to the OCaml binary")
51 | args = parser.parse_args()
52 | sys.exit(main(args.files, args.draw_cfg, args.binary))
53 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp25.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module TI = Tok_info
5 | module S = Syntax
6 |
7 | let types_can_be_casted ty_from ty_to =
8 | let open S in
9 | match (ty_from, ty_to) with
10 | | (DTyDeclSingleElement(se_from_spec,_),DTyDeclSingleElement(se_to_spec,_)) -> begin
11 | match (se_from_spec, se_to_spec) with
12 | | (DTySpecElementary(REAL|LREAL),DTySpecElementary(SINT|INT|DINT|LINT|USINT|UDINT|ULINT|BOOL|BYTE|WORD|DWORD|LWORD)) -> false
13 | | (DTySpecElementary(SINT|INT|DINT|LINT|USINT|UDINT|ULINT|BOOL|BYTE|WORD|DWORD|LWORD),DTySpecElementary(REAL|LREAL)) -> false
14 | | _ -> true
15 | end
16 | | _ -> true
17 |
18 | let check_assign_expr (ti : TI.t) lhs rhs env =
19 | let check_types lhs_decl rhs_decl =
20 | match (S.VarDecl.get_ty_spec lhs_decl, S.VarDecl.get_ty_spec rhs_decl) with
21 | | (Some(lhs_ty),Some(rhs_ty)) -> begin
22 | if not (types_can_be_casted lhs_ty rhs_ty) then
23 | Some(Warn.mk ti.linenr ti.col "PLCOPEN-CP25" "Data type conversion should be explicit.")
24 | else
25 | None
26 | end
27 | | _ -> None
28 | in
29 | let lhs_opt = Env.lookup_vdecl env (S.VarUse.get_name lhs)
30 | and rhs_opt = Env.lookup_vdecl env (S.VarUse.get_name rhs)
31 | in
32 | match (lhs_opt,rhs_opt) with
33 | | (Some(lhs), Some(rhs)) -> check_types lhs rhs
34 | | _ -> None
35 |
36 | let check_pou pou env =
37 | Ast_util.get_pou_exprs pou
38 | |> List.fold_left
39 | ~init:[]
40 | ~f:(fun acc expr -> begin
41 | match expr with
42 | | S.ExprBin (ti,(S.ExprVariable (_, lhs)),(S.EQ|S.NEQ|S.ASSIGN|S.ASSIGN_REF|S.GT|S.LT|S.GE|S.LE|S.SENDTO),(S.ExprVariable (_, rhs))) -> begin
43 | check_assign_expr ti lhs rhs env
44 | |> Caml.Option.fold ~none:[] ~some:(fun w -> [w])
45 | |> List.append acc
46 | end
47 | | _ -> acc
48 | end)
49 |
50 | let do_check elems envs =
51 | List.fold_left
52 | elems
53 | ~init:[]
54 | ~f:(fun acc pou -> begin
55 | let env = List.find_exn envs
56 | ~f:(fun env -> phys_equal (Env.get_id env) (S.get_pou_id pou))
57 | in
58 | acc @ check_pou pou env
59 | end)
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build files
2 | .merlin
3 | *.install
4 |
5 | # Parser files
6 | *.conflicts
7 | *.automaton
8 | *messages_complete.txt
9 |
10 | .#*
11 |
12 | src/lexer*.ml
13 | src/parser.ml
14 | src/parser.mli
15 | iec_checker.native
16 |
17 | doc/grammar.html
18 |
19 | _build/
20 | .dump.json
21 | # Byte-compiled / optimized / DLL files
22 | __pycache__/
23 | *.py[cod]
24 | *$py.class
25 |
26 | # C extensions
27 | *.so
28 |
29 | # Distribution / packaging
30 | .Python
31 | build/
32 | develop-eggs/
33 | dist/
34 | downloads/
35 | eggs/
36 | .eggs/
37 | lib64/
38 | parts/
39 | sdist/
40 | var/
41 | wheels/
42 | *.egg-info/
43 | .installed.cfg
44 | *.egg
45 | MANIFEST
46 |
47 | # PyInstaller
48 | # Usually these files are written by a python script from a template
49 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
50 | *.manifest
51 | *.spec
52 |
53 | # Installer logs
54 | pip-log.txt
55 | pip-delete-this-directory.txt
56 |
57 | # Unit test / coverage reports
58 | htmlcov/
59 | .tox/
60 | .nox/
61 | .coverage
62 | .coverage.*
63 | .cache
64 | nosetests.xml
65 | coverage.xml
66 | *.cover
67 | .hypothesis/
68 | .pytest_cache/
69 |
70 | # Translations
71 | *.mo
72 | *.pot
73 |
74 | # Django stuff:
75 | *.log
76 | local_settings.py
77 | db.sqlite3
78 |
79 | # Flask stuff:
80 | instance/
81 | .webassets-cache
82 |
83 | # Scrapy stuff:
84 | .scrapy
85 |
86 | # Sphinx documentation
87 | docs/_build/
88 |
89 | # PyBuilder
90 | target/
91 |
92 | # Jupyter Notebook
93 | .ipynb_checkpoints
94 |
95 | # IPython
96 | profile_default/
97 | ipython_config.py
98 |
99 | # pyenv
100 | .python-version
101 |
102 | # celery beat schedule file
103 | celerybeat-schedule
104 |
105 | # SageMath parsed files
106 | *.sage.py
107 |
108 | # Environments
109 | .env
110 | .venv
111 | env/
112 | venv/
113 | ENV/
114 | env.bak/
115 | venv.bak/
116 |
117 | # Spyder project settings
118 | .spyderproject
119 | .spyproject
120 |
121 | # Rope project settings
122 | .ropeproject
123 |
124 | # mkdocs documentation
125 | /site
126 |
127 | # mypy
128 | .mypy_cache/
129 | .dmypy.json
130 | dmypy.json
131 |
132 | # Pyre type checker
133 | .pyre/
134 |
135 | output
136 | assets
137 |
--------------------------------------------------------------------------------
/src/core/cfg.mli:
--------------------------------------------------------------------------------
1 | (** Control Flow Graph for Intraprocedural Analysis.
2 |
3 | The current implementation of CFG is used Syntax.stmt as nodes and store
4 | connections (edges) between them. This is useful for dead code detection.
5 | *)
6 | module S = Syntax
7 | module TI = Tok_info
8 |
9 | type t
10 |
11 | (** Type of a basic block. *)
12 | type bb_ty =
13 | | BB (** Regular basic block *)
14 | | BBEntry (** Point of entry *)
15 | | BBExit (** Point of exit *)
16 | | BBJump (** Indirect jump to/from a node *)
17 |
18 | (** Basic block *)
19 | type bb =
20 | {
21 | id : int;
22 | mutable ty: bb_ty;
23 | mutable preds : int list; (** Ids of predecessor nodes *)
24 | mutable succs : int list; (** Ids of successor nodes *)
25 | (* TODO: This should be replaced with ids of statements. But it will
26 | require additional symbol tables and a lot of refactoring in the parser. *)
27 | mutable stmts : S.statement list [@opaque]; (** Statements that makes up this BB *)
28 | }
29 |
30 | val mk : S.iec_library_element -> t
31 | (** [mk] Create a new CFG instance for a given iec_library_element. *)
32 |
33 | val get_pou_id : t -> int
34 | (** [get_pou_id] Get the id of the POU that this CFG belongs to. *)
35 |
36 | val get_bb_by_id_exn : t -> int -> bb
37 | (** [get_bb_by_id_exn id cfg] Return basic block stored in [cfg] by given ID.
38 | Raise [Not_found] if there are no such block *)
39 |
40 | val get_all_ids : t -> int list
41 | (** [get_all_ids cfg] Return a list with ids of all basic blocks represented
42 | in [cfg]. *)
43 |
44 | val get_reachable_ids : t -> int list
45 | (** [get_reachable_ids cfg] Return a list with ids of basic blocks that are
46 | reachable from [cfg] entry point. *)
47 |
48 | val get_number_of_edges : t -> int
49 | (** [get_number_of_edges cfg] Return number of edges in [cfg]. *)
50 |
51 | val bb_by_id : t -> int -> bb option
52 | (** [bb_by_id] Get basic block entry from a given id. *)
53 |
54 | val bb_get_ti : bb -> TI.t
55 | (** [bb_get_ti] Get token info for the basic block. *)
56 |
57 | val create_cfgs : S.iec_library_element list -> t list
58 | (** [create_cfgs] Create list of CFGs for a given iec_library_element objects. *)
59 |
60 | val to_string : t -> string
61 |
62 | val to_yojson : t -> Yojson.Safe.t
63 |
64 |
--------------------------------------------------------------------------------
/src/lib/checkerLib.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = IECCheckerCore.Syntax
3 | module Env = IECCheckerCore.Env
4 | module TI = IECCheckerCore.Tok_info
5 | module Config = IECCheckerCore.Config
6 |
7 | let print_element (e : S.iec_library_element) =
8 | match e with
9 | | S.IECFunction (_, f) ->
10 | Printf.printf "Running check for function %s\n" (S.Function.get_name f.id)
11 | | S.IECFunctionBlock (_, fb) ->
12 | Printf.printf "Running check for function block %s\n"
13 | (S.FunctionBlock.get_name fb.id)
14 | | S.IECProgram (_, p) -> Printf.printf "Running check for program %s\n" p.name
15 | | S.IECClass (_, c) -> Printf.printf "Running check for class %s\n" c.class_name
16 | | S.IECInterface (_, i) -> Printf.printf "Running check for interafece %s\n" i.interface_name
17 | | S.IECConfiguration (_, c) ->
18 | Printf.printf "Running check for configuration %s\n" c.name
19 | | S.IECType _ ->
20 | Printf.printf "Running check for derived type\n"
21 |
22 | let run_all_checks elements envs cfgs quiet =
23 | if not quiet then
24 | List.iter elements ~f:(fun e -> print_element e);
25 | []
26 | |> List.append (if Config.check_plcopen_cp1 then Plcopen_cp1.do_check elements else [])
27 | |> List.append (if Config.check_plcopen_cp2 then Plcopen_cp2.do_check cfgs else [])
28 | |> List.append (if Config.check_plcopen_cp3 then Plcopen_cp3.do_check elements else [])
29 | |> List.append (if Config.check_plcopen_cp4 then Plcopen_cp4.do_check elements else [])
30 | |> List.append (if Config.check_plcopen_cp6 then Plcopen_cp6.do_check elements else [])
31 | |> List.append (if Config.check_plcopen_cp8 then Plcopen_cp8.do_check elements else [])
32 | |> List.append (if Config.check_plcopen_cp9 then Plcopen_cp9.do_check elements cfgs else [])
33 | |> List.append (if Config.check_plcopen_cp13 then Plcopen_cp13.do_check elements else [])
34 | |> List.append (if Config.check_plcopen_cp25 then Plcopen_cp25.do_check elements envs else [])
35 | |> List.append (if Config.check_plcopen_cp28 then Plcopen_cp28.do_check elements else [])
36 | |> List.append (if Config.check_plcopen_l10 then Plcopen_l10.do_check elements else [])
37 | |> List.append (if Config.check_plcopen_l17 then Plcopen_l17.do_check elements else [])
38 | |> List.append (if Config.check_plcopen_n3 then Plcopen_n3.do_check elements else [])
39 | |> List.append (Zerodiv.do_check elements)
40 |
--------------------------------------------------------------------------------
/src/python/core.py:
--------------------------------------------------------------------------------
1 | """
2 | Module to communicate with the compiled OCaml binary.
3 | """
4 | from typing import List, Tuple
5 | import io
6 | import os
7 | import subprocess
8 | import ijson
9 |
10 | from .om import Warning
11 |
12 | binary_default = os.path.join("..", "bin", "iec_checker")
13 |
14 |
15 | def process_output(json_out: bytes) -> List[Warning]:
16 | warnings = []
17 | for warns in ijson.items(io.BytesIO(json_out), ""):
18 | for item in warns:
19 | if item:
20 | warnings.append(Warning.from_dict(item))
21 | return warnings
22 |
23 |
24 | def check_program(program: str,
25 | binary: str = binary_default) -> Tuple[List[Warning], int]:
26 | """Runs ``iec-checker`` and sends the given program source in stdin.
27 | This will create 'stdin.dump.json' dump file in the current directory.
28 | Returns the list of warnings and the return code.
29 | """
30 | p = subprocess.Popen([binary, "-o", "json", "-q", "-d", "-"],
31 | stdout=subprocess.PIPE,
32 | stderr=subprocess.STDOUT,
33 | stdin=subprocess.PIPE,
34 | encoding='utf8')
35 | out, err = p.communicate(f'{program}\n')
36 | p.wait()
37 | warnings = process_output(out.encode())
38 | return (warnings, p.returncode)
39 |
40 |
41 | def run_checker(paths: str, binary: str = binary_default,
42 | args: List[str] = []) -> Tuple[List[Warning], int]:
43 | """Runs ``iec-checker`` for the given input files.
44 | This will execute analyses and generate JSON dump processed by plugins."""
45 | p = subprocess.Popen([binary, "-o", "json", "-q", "-d", *args, *paths],
46 | stdout=subprocess.PIPE,
47 | stderr=subprocess.STDOUT)
48 | p.wait()
49 | out, err = p.communicate()
50 | warnings = process_output(out)
51 | return (warnings, p.returncode)
52 |
53 |
54 | def run_checker_full_out(paths: str, binary: str = binary_default,
55 | *args) -> Tuple[int, str]:
56 | """Runs ``iec-checker`` for the given input files and captures its output.
57 | No extra options will be set by default."""
58 | p = subprocess.Popen([binary, *args, *paths],
59 | stdout=subprocess.PIPE,
60 | stderr=subprocess.STDOUT)
61 | p.wait()
62 | out, err = p.communicate()
63 | return (p.returncode, '\n'.join([str(out), str(err)]))
64 |
65 |
66 | def filter_warns(warns: List[Warning], warn_id: str) -> List[Warning]:
67 | """Filter warning by identifier."""
68 | return list(filter(lambda w: w.id == warn_id, warns))
69 |
--------------------------------------------------------------------------------
/src/core/sel.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = Syntax
3 |
4 | type pou_ty =
5 | | FB
6 | | Function
7 | | Program
8 |
9 | type parse_result = {
10 | mutable interface: string option;
11 | mutable implementation: string option;
12 | mutable pou_type: pou_ty option;
13 | }
14 |
15 | (** Parses the text string from the XMLM node. *)
16 | let pull_data i =
17 | if Xmlm.eoi i then None
18 | else
19 | match Xmlm.input i with
20 | | `Data d -> Some(d)
21 | | _ -> None
22 |
23 | let pull_pou_type i =
24 | if Xmlm.eoi i then None
25 | else
26 | match Xmlm.input i with
27 | | `Data d -> begin
28 | if (String.equal d "FunctionBlock") then
29 | Some(FB)
30 | else if (String.equal d "Function") then
31 | Some(Function)
32 | else if (String.equal d "Program") then
33 | Some(Program)
34 | else
35 | None
36 | end
37 | | _ -> None
38 | let get_end_tag = function
39 | | FB -> "END_FUNCTION_BLOCK"
40 | | Function -> "END_FUNCTION"
41 | | Program -> "END_PROGRAM"
42 |
43 | (** Creates a source code from the parse_result entry and returns the source code. *)
44 | let reconstruct_source res : (string option) =
45 | match res.interface, res.implementation, res.pou_type with
46 | | Some(interface), Some(impl), Some(pou_type) ->
47 | Some(String.concat ~sep:"\n" [interface; impl; (get_end_tag pou_type)])
48 | | _ -> None
49 |
50 |
51 | (** Iterate over all XML elements in schema to parse their source code. *)
52 | let rec parse_source i d acc =
53 | if Xmlm.eoi i then reconstruct_source acc
54 | else
55 | match Xmlm.input i with
56 | |`El_start ((_, tag), _) when (String.equal tag "Interface") -> begin
57 | acc.interface <- (pull_data i);
58 | parse_source i d acc
59 | end
60 | |`El_start ((_, tag), _) when (String.equal tag "Implementation") -> begin
61 | acc.implementation <- (pull_data i);
62 | parse_source i d acc
63 | end
64 | |`El_start ((_, tag), _) when (String.equal tag "POUKind" || String.equal tag "Type") -> begin
65 | acc.pou_type <- (pull_pou_type i);
66 | parse_source i d acc
67 | end
68 | |`El_start _ -> parse_source i (d + 1) acc
69 | | `El_end -> if (phys_equal d 1) then reconstruct_source acc else parse_source i (d - 1) acc
70 | | _ -> parse_source i d acc
71 |
72 | let reconstruct_from_channel_opt ic =
73 | let i_opt = try Some(Xmlm.make_input (`Channel ic)) with
74 | | _ -> None
75 | in
76 | match i_opt with
77 | | None -> None
78 | | Some(i) -> begin
79 | let result = { interface = None;
80 | implementation = None;
81 | pou_type = None } in
82 | try (parse_source i 1 result) with
83 | | _ -> None
84 | end
85 |
--------------------------------------------------------------------------------
/test/st/good/literals.st:
--------------------------------------------------------------------------------
1 | (* Test lexing/parsing of literals. *)
2 | PROGRAM program0
3 | VAR
4 | a : ULINT;
5 | b : BOOL;
6 | r : REAL;
7 | t : TIME;
8 | td : TOD;
9 | d : DATE;
10 | dtv : DATE_AND_TIME;
11 |
12 | (* The following identifiers are not defined as reserved keywords in the standard.
13 | So they could be used as a regular identifiers. *)
14 | T : INT;
15 | LT : INT;
16 | D : INT;
17 | LD : INT;
18 |
19 | s : STRING;
20 | END_VAR
21 |
22 | (* integer *)
23 | a := 3;
24 | a := 3_000;
25 | a := 3_000_000;
26 | a := 3_00_0000;
27 | a := ULINT#3;
28 | a := ULINT#3_000;
29 |
30 | (* binary_integer *)
31 | a := 2#1;
32 | a := 2#010110;
33 | a := 2#11_011_0;
34 | a := ULINT#2#1;
35 | a := ULINT#2#1_000;
36 |
37 | (* octal_integer *)
38 | a := 8#7;
39 | a := 8#71_061_0;
40 | a := ULINT#8#7;
41 | a := ULINT#8#7_000;
42 |
43 | (* hex_integer *)
44 | a := 16#0;
45 | a := 16#10;
46 | a := 16#A;
47 | a := 16#A_B_C;
48 | a := ULINT#16#A;
49 | a := ULINT#16#A_B00;
50 |
51 | (* bool_literal *)
52 | b := 1;
53 | b := 0;
54 | b := FALSE;
55 | b := TRUE;
56 | b := BOOL#1;
57 | b := BOOL#0;
58 | b := BOOL#FALSE;
59 | b := BOOL#TRUE;
60 |
61 | (* real_lieral *)
62 | r := 1.0;
63 | r := 0.0;
64 | r := 3.000_300;
65 | r := +3.2;
66 | r := 3.2E1;
67 | r := +3.2E1;
68 | r := -3.2E1;
69 | r := -3.2E+1;
70 | r := -3.2E-1;
71 | r := REAL#2.0;
72 | r := REAL#-2.0;
73 | r := REAL#-2.0E8;
74 | r := REAL#-2.0E-8;
75 |
76 | (* time_literal *)
77 | t := TIME#3.0d;
78 | t := TIME#3d;
79 | t := TIME#3.0h;
80 | t := TIME#3.0m;
81 | t := TIME#3.0s;
82 | t := TIME#3.0ms;
83 | t := TIME#3.0us;
84 | t := TIME#3.0ns;
85 | t := TIME#3.0d 4.0h;
86 | t := TIME#3.0h3m;
87 | t := LTIME#3.0d;
88 | t := T#3.0d;
89 | t := LT#3.0d;
90 | t := TIME#3.0d;
91 |
92 | (* time_of_day *)
93 | td := TOD#03:12:20;
94 | td := LTOD#03:12:20;
95 | td := TIME_OF_DAY#03:12:20;
96 | td := LTIME_OF_DAY#03:12:20;
97 | td := LTIME_OF_DAY#03:12:20.5;
98 |
99 | (* date *)
100 | d := DATE#2020-01-30;
101 | d := LDATE#2020-01-30;
102 | d := D#2020-01-30;
103 | d := LD#2020-01-30;
104 |
105 | (* date_and_time *)
106 | dtv := LDT#2020-01-30-18:01:38;
107 | dtv := LDT#2020-01-30-18:01:38.33;
108 | dtv := DT#2020-01-30-18:01:38;
109 | dtv := DATE_AND_TIME#2020-01-30-18:01:38;
110 | dtv := LDATE_AND_TIME#2020-01-30-18:01:38;
111 |
112 | (* String literals *)
113 | s := "FOO";
114 | s := "(*FO*)O";
115 | s := 'BAR';
116 | s := STRING#"FOO";
117 | s := STRING#"BAR(*";
118 | s := STRING#'FOO';
119 |
120 | END_PROGRAM
121 | (* vim: set foldmethod=marker foldlevel=0 foldenable sw=2 tw=120 : *)
122 |
--------------------------------------------------------------------------------
/test/st/good/control-statements.st:
--------------------------------------------------------------------------------
1 | (* This file contains various control statements of the ST language. *)
2 |
3 | (* Table 72 -- №4 *)
4 | PROGRAM square_root
5 | VAR
6 | a, b, c, d : REAL;
7 | x1, x2 : REAL;
8 | nroots : INT;
9 | END_VAR
10 |
11 | d := b*b - 4.0*a;
12 | d := b*b - 4.0*a*c;
13 | IF d < 0.0
14 | THEN nroots := 0;
15 | ELSIF d = 0.0
16 | THEN
17 | nroots := 1;
18 | X1 := -b/(2.0*A);
19 | X1 := -b;
20 | ELSE
21 | nroots := 2;
22 | X1 := (-b + SQRT(d))/(2.0*a);
23 | X2 := (-b - SQRT(d))/(2.0*a);
24 | END_IF;
25 |
26 | END_PROGRAM
27 |
28 | PROGRAM test_for
29 | VAR
30 | i, j : INT;
31 | flag : INT;
32 | counter : INT := 0;
33 | some_var : INT;
34 | END_VAR
35 |
36 | FOR i := 0 TO 10 DO
37 | FOR j := 10 TO 100 BY 2 DO
38 | IF flag THEN
39 | EXIT;
40 | END_IF;
41 | counter := counter + 1;
42 | IF j = 10 THEN
43 | CONTINUE;
44 | END_IF;
45 | END_FOR;
46 | END_FOR;
47 | END_PROGRAM
48 |
49 | PROGRAM test_switch_case
50 | VAR
51 | TW : INT;
52 | THUMBWHEEL : WORD;
53 | TW_ERROR : INT;
54 | some_var : INT;
55 | END_VAR
56 |
57 | TW := WORD_BCD_TO_INT(THUMBWHEEL);
58 | TW_ERROR:= 0;
59 | CASE TW OF
60 | 1,5: DISPLAY := OVEN_TEMP;
61 | 2: DISPLAY := MOTOR_SPEED;
62 | 3: DISPLAY := GROSS - TARE;
63 | 4,6..10: DISPLAY := STATUS(TW-4);
64 | ELSE DISPLAY := 0;
65 | TW_ERROR:= 1;
66 | END_CASE;
67 | QW100:= INT_TO_BCD(DISPLAY);
68 | END_PROGRAM
69 |
70 | FUNCTION fn0 : INT
71 | VAR_INPUT
72 | INVAL : INT;
73 | END_VAR
74 | VAR_OUTPUT
75 | OUTVAL : INT;
76 | END_VAR
77 |
78 | OUTVAL := 0;
79 | END_FUNCTION
80 |
81 | PROGRAM p0
82 | VAR_INPUT
83 | vi1 : INT;
84 | END_VAR
85 | VAR_OUTPUT
86 | vo1 : INT;
87 | vo2 : INT;
88 | END_VAR
89 | VAR
90 | i : INT;
91 | x : INT;
92 | acc : INT;
93 | j : INT;
94 | END_VAR
95 |
96 | IF (vi1 = 0)
97 | THEN vo1 := 0;
98 | ELSE vo1 := vi1 - 42;
99 | END_IF;
100 |
101 | CASE vi1 OF
102 | 1 : vo1 := 19;
103 | 2 : vo1 := 29;
104 | 3,4 : vo1 := 39;
105 | (* TODO: 3..10: vo1 := 42; *)
106 | ELSE vo1 := 1; vo2 := -1;
107 | END_CASE;
108 |
109 | FOR i := 1 TO 100 DO
110 | acc := acc * x;
111 | END_FOR;
112 |
113 | FOR i := 100 TO 0 BY -2 DO
114 | acc := acc * x;
115 | END_FOR;
116 |
117 | J := 1;
118 | WHILE J <= 100 DO
119 | J := J + 2;
120 | END_WHILE;
121 |
122 | J := -1;
123 | REPEAT
124 | J := J + 2;
125 | UNTIL J = 101
126 | END_REPEAT;
127 |
128 | (* Invocation statements *)
129 | acc := fn0();
130 | acc := fn0(19);
131 | acc := fn0(INVAL := 19);
132 |
133 | END_PROGRAM
134 |
--------------------------------------------------------------------------------
/test/test_cfa.py:
--------------------------------------------------------------------------------
1 | """Tests for control flow analysis inspections provided by OCaml core."""
2 | import sys
3 | import os
4 | import pytest
5 |
6 | sys.path.append(os.path.join(os.path.dirname(
7 | os.path.abspath(__file__)), "../src"))
8 | from python.core import check_program, filter_warns # noqa
9 | from python.dump import DumpManager # noqa
10 |
11 |
12 | def test_cfa_dead_code_top_statements():
13 | fdump = f'stdin.dump.json'
14 | warns, rc = check_program(
15 | """
16 | FUNCTION test_dead_code_to_stmts : INT
17 | VAR
18 | counter : INT := 0;
19 | some_var : INT;
20 | END_VAR
21 | counter := counter + 1;
22 | counter := 2 + 2;
23 | RETURN;
24 | some_var := SQRT(16#42); (* UnreachableCode error *)
25 | some_var := 16#42; (* No additional warnings *)
26 | some_var := 19;
27 | END_FUNCTION
28 | """.replace('\n', ''))
29 | assert rc == 0
30 | assert len(warns) >= 1
31 | assert len(filter_warns(warns, 'PLCOPEN-CP2')) == 1
32 | with DumpManager(fdump) as dm:
33 | scheme = dm.scheme
34 | assert scheme
35 |
36 |
37 | def test_cfa_dead_code_in_the_loops():
38 | fdump = f'stdin.dump.json'
39 | warns, rc = check_program(
40 | """
41 | PROGRAM dead_code_in_the_loops
42 | VAR a : INT; i : INT; END_VAR
43 | WHILE i < 10 DO
44 | IF i = 5 THEN
45 | i := i + 1;
46 | EXIT;
47 | i := 19; (* UnreachableCode error *)
48 | i := 42; (* No additional warnings *)
49 | i := 42;
50 | ELSIF i = 6 THEN
51 | CONTINUE;
52 | i := 3; (* UnreachableCode error *)
53 | i := 44; (* No additional warnings *)
54 | i := 19;
55 | END_IF;
56 | i := i + 2;
57 | END_WHILE;
58 | i := 0;
59 | END_PROGRAM
60 | """.replace('\n', ''))
61 | assert rc == 0
62 | assert len(filter_warns(warns, 'PLCOPEN-CP2')) == 2
63 | with DumpManager(fdump) as dm:
64 | scheme = dm.scheme
65 | assert scheme
66 |
67 |
68 | def test_cfa_multiple_pous():
69 | fdump = f'stdin.dump.json'
70 | warns, rc = check_program(
71 | """
72 | FUNCTION dead_code_after_return_1 : INT
73 | VAR some_var : INT; END_VAR
74 | RETURN;
75 | some_var := SQRT(16#42); (* UnreachableCode error *)
76 | END_FUNCTION
77 |
78 | FUNCTION dead_code_after_return_2 : INT
79 | VAR some_var : INT; END_VAR
80 | RETURN;
81 | some_var := SQRT(16#42); (* UnreachableCode error *)
82 | END_FUNCTION
83 | """.replace('\n', ''))
84 | assert rc == 0
85 | assert len(filter_warns(warns, 'PLCOPEN-CP2')) == 2
86 | with DumpManager(fdump) as dm:
87 | scheme = dm.scheme
88 | assert scheme
89 |
--------------------------------------------------------------------------------
/test/test_plcopen.py:
--------------------------------------------------------------------------------
1 | """Tests for PLCOpen inspections."""
2 | import sys
3 | import os
4 |
5 | sys.path.append(os.path.join(os.path.dirname(
6 | os.path.abspath(__file__)), "../src"))
7 | from python.core import run_checker, filter_warns # noqa
8 | from python.dump import DumpManager # noqa
9 |
10 |
11 | def test_cp1():
12 | f = 'st/plcopen-cp1.st'
13 | fdump = f'{f}.dump.json'
14 | checker_warnings, rc = run_checker([f])
15 | assert rc == 0
16 | checker_warnings.count('PLCOPEN-CP1') == 1
17 | with DumpManager(fdump):
18 | pass
19 |
20 |
21 | def test_cp3():
22 | f = 'st/plcopen-cp3.st'
23 | fdump = f'{f}.dump.json'
24 | checker_warnings, rc = run_checker([f])
25 | assert rc == 0
26 | checker_warnings.count('PLCOPEN-CP3') == 8
27 | with DumpManager(fdump):
28 | pass
29 |
30 |
31 | def test_cp6():
32 | f = 'st/plcopen-cp6.st'
33 | fdump = f'{f}.dump.json'
34 | checker_warnings, rc = run_checker([f])
35 | assert rc == 0
36 | checker_warnings.count('PLCOPEN-CP6') == 2
37 | with DumpManager(fdump):
38 | pass
39 |
40 |
41 | def test_cp8():
42 | f = 'st/plcopen-cp8.st'
43 | fdump = f'{f}.dump.json'
44 | checker_warnings, rc = run_checker([f])
45 | assert rc == 0
46 | checker_warnings.count('PLCOPEN-CP8') == 4
47 | with DumpManager(fdump):
48 | pass
49 |
50 |
51 | def test_cp28():
52 | f = 'st/plcopen-cp28.st'
53 | fdump = f'{f}.dump.json'
54 | checker_warnings, rc = run_checker([f])
55 | assert rc == 0
56 | checker_warnings.count('PLCOPEN-CP28') == 4
57 | with DumpManager(fdump):
58 | pass
59 |
60 |
61 | def test_cp13():
62 | f = 'st/plcopen-cp13.st'
63 | fdump = f'{f}.dump.json'
64 | checker_warnings, rc = run_checker([f])
65 | assert rc == 0
66 | checker_warnings.count('PLCOPEN-CP13') == 3
67 | with DumpManager(fdump):
68 | pass
69 |
70 |
71 | def test_cp25():
72 | f = 'st/plcopen-cp25.st'
73 | fdump = f'{f}.dump.json'
74 | checker_warnings, rc = run_checker([f])
75 | assert rc == 0
76 | checker_warnings.count('PLCOPEN-CP25') == 2
77 | with DumpManager(fdump):
78 | pass
79 |
80 |
81 | def test_l10():
82 | f = 'st/plcopen-l10.st'
83 | fdump = f'{f}.dump.json'
84 | checker_warnings, rc = run_checker([f])
85 | assert rc == 0
86 | checker_warnings.count('PLCOPEN-L10') == 3
87 | with DumpManager(fdump):
88 | pass
89 |
90 |
91 | def test_l17():
92 | f = 'st/plcopen-l17.st'
93 | fdump = f'{f}.dump.json'
94 | checker_warnings, rc = run_checker([f])
95 | assert rc == 0
96 | assert len(checker_warnings) >= 1
97 | cv = checker_warnings[1]
98 | assert cv.id == 'PLCOPEN-L17'
99 | assert cv.linenr == 10
100 | assert cv.column == 4
101 | with DumpManager(fdump):
102 | pass
103 |
104 |
105 | def test_n3():
106 | f = 'st/plcopen-n3.st'
107 | fdump = f'{f}.dump.json'
108 | checker_warnings, rc = run_checker([f])
109 | assert rc == 0
110 | assert len(checker_warnings) >= 1
111 | cv = checker_warnings[5]
112 | assert cv.id == 'PLCOPEN-N3'
113 | assert cv.linenr == 6
114 | assert cv.column == 7
115 | with DumpManager(fdump):
116 | pass
117 |
118 |
119 | def test_cp9():
120 | f = 'st/plcopen-cp9.st'
121 | fdump = f'{f}.dump.json'
122 | warns, rc = run_checker([f])
123 | assert rc == 0
124 | assert len(filter_warns(warns, 'PLCOPEN-CP9')) == 2
125 | with DumpManager(fdump):
126 | pass
127 |
--------------------------------------------------------------------------------
/src/analysis/declaration_analysis.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module AU = IECCheckerCore.Ast_util
3 | module S = IECCheckerCore.Syntax
4 | module Env = IECCheckerCore.Env
5 | module Warn = IECCheckerCore.Warn
6 |
7 | let str_len = function
8 | | S.STRING l -> l
9 | | S.WSTRING l -> l
10 | | S.CHAR l -> l
11 | | S.WCHAR l -> l
12 | | _ -> assert false
13 |
14 | (** Compare length of declared string with initializer string size. *)
15 | let check_str_init_size ty_init init_expr =
16 | let check_length init_str =
17 | let ty_len = str_len ty_init in
18 | let init_len = String.length init_str in
19 | if ty_len <> init_len then Some (ty_len, init_len) else None
20 | in
21 | match init_expr with
22 | | Some(e) ->
23 | begin
24 | match e with
25 | | S.ExprConstant(_,c) ->
26 | begin
27 | match c with
28 | | S.CString(_, str) -> check_length str
29 | | _ -> None
30 | end
31 | | _ -> None
32 | end
33 | | None -> None
34 |
35 | (** Search for errors in initial value in declaration of a string type *)
36 | let check_str_init_expr ty_init init_expr =
37 | match check_str_init_size ty_init init_expr with
38 | | Some (len_decl, len_init) when (len_init > len_decl) ->
39 | let msg =
40 | Printf.sprintf
41 | "Length of initialization string literal exceeds string length (%d > %d)"
42 | len_init len_decl
43 | in
44 | let w = Warn.mk 0 0 "OutOfBounds" msg in
45 | [ w ]
46 | | Some _ -> [] (* no violations *)
47 | | None -> []
48 |
49 | (** Search for errors in subrange initialization *)
50 | let check_subrange_init_val ty_spec init_val =
51 | let (_, lb, ub) = ty_spec in
52 | if (init_val < lb) || (init_val > ub) then
53 | let msg =
54 | Printf.sprintf "Initial subrange value %d does not fit the specified range (%d .. %d)"
55 | init_val lb ub
56 | in let w = Warn.mk 0 0 "OutOfBounds" msg in [ w ]
57 | else []
58 |
59 | (** Search for errors in array initialization. *)
60 | let check_array_init_val ty_name subranges inval_opt =
61 | match inval_opt with
62 | | None -> (* no initializer list *) []
63 | | Some (inlist) -> begin
64 | let dimensions_capacity = AU.eval_array_capacity subranges in
65 | let diff = (List.length inlist) - dimensions_capacity in
66 | if diff > 0 then begin
67 | let m = Printf.sprintf
68 | "%s: Array initializer list exceeds capacity. Last %d values will be lost."
69 | ty_name diff
70 | in
71 | [(Warn.mk 0 0 "OutOfBounds" m)]
72 | end
73 | else
74 | []
75 | end
76 |
77 | let check_ty_decl ty_name = function
78 | | S.DTyDeclSingleElement (ty_spec, init_expr) ->
79 | begin
80 | match ty_spec with
81 | | S.DTySpecElementary ty_decl -> (check_str_init_expr ty_decl init_expr)
82 | | S.DTySpecSimple _ | S.DTySpecGeneric _ | S.DTySpecEnum _ -> []
83 | end
84 | | S.DTyDeclSubrange (ty_spec, init_val) -> check_subrange_init_val ty_spec init_val
85 | | S.DTyDeclEnumType _ -> []
86 | | S.DTyDeclArrayType (subranges, _, inval_opt) -> check_array_init_val ty_name subranges inval_opt
87 | | S.DTyDeclRefType _ -> []
88 | | S.DTyDeclStructType _ -> []
89 |
90 | (** [check_var_decls pou] Searching for errors in variables declaration for the
91 | given [pou]. *)
92 | let check_var_decls pou =
93 | AU.get_var_decls pou
94 | |> List.fold_left
95 | ~init:[]
96 | ~f:(fun acc var_decl -> begin
97 | let ty_decl_opt = S.VarDecl.get_ty_spec var_decl
98 | and var_name = S.VarDecl.get_var_name var_decl in
99 | match ty_decl_opt with
100 | | Some (ty_decl) -> acc @ (check_ty_decl var_name ty_decl)
101 | | None -> acc
102 | end)
103 |
104 | let[@warning "-27"] run elements envs =
105 | List.fold_left elements
106 | ~f:(fun warns e ->
107 | let ws = match e with
108 | | S.IECType (_, (ty_name, ty_spec)) -> check_ty_decl ty_name ty_spec
109 | | _ -> check_var_decls e
110 | in
111 | warns @ ws)
112 | ~init:[]
113 |
--------------------------------------------------------------------------------
/src/lib/plcopen_n3.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = IECCheckerCore.Syntax
3 | module TI = IECCheckerCore.Tok_info
4 | module AU = IECCheckerCore.Ast_util
5 | module Warn = IECCheckerCore.Warn
6 |
7 | (** Keywords / reserved word list of IEC 61131-3 Ed.3 starting with a letter *)
8 | let reserved_keywords =
9 | [
10 | "ABS";
11 | "END_IF";
12 | "ABSTRACT";
13 | "END_INTERFACE LEFT";
14 | "ACOS";
15 | "END_METHOD";
16 | "LEN";
17 | "ACTION";
18 | "END_NAMESPACE LIMIT";
19 | "ADD";
20 | "END_PROGRAM";
21 | "LINT";
22 | "AND";
23 | "END_REPEAT";
24 | "LN";
25 | "ARRAY";
26 | "END_RESOURCE LOG";
27 | "ASIN";
28 | "END_STEP";
29 | "LREAL";
30 | "AT";
31 | "END_STRUCT";
32 | "LT";
33 | "ATAN";
34 | "END_TRANSITION LTIME";
35 | "ATAN2";
36 | "END_TYPE";
37 | "LTIME_OF_DAY";
38 | "BOOL";
39 | "END_VAR";
40 | "LTOD";
41 | "BY";
42 | "END_WHILE";
43 | "LWORD";
44 | "BYTE";
45 | "EQ";
46 | "MAX";
47 | "CASE";
48 | "EXIT";
49 | "METHOD";
50 | "CHAR";
51 | "EXP";
52 | "MID";
53 | "CLASS";
54 | "EXPT";
55 | "MIN";
56 | "CONCAT";
57 | "EXTENDS";
58 | "MOD";
59 | "CONFIGURATION";
60 | "F_EDGE";
61 | "MOVE";
62 | "CONSTANT";
63 | "F_TRIG";
64 | "MUL";
65 | "CONTINUE";
66 | "FALSE";
67 | "MUX";
68 | "COS";
69 | "FINAL";
70 | "NAMESPACE";
71 | "CTD";
72 | "FIND";
73 | "NE";
74 | "CTU";
75 | "FOR";
76 | "NON_RETAIN";
77 | "CTUD";
78 | "FROM";
79 | "NOT";
80 | "DATE";
81 | "FUNCTION";
82 | "NULL";
83 | "DATE_AND_TIME";
84 | "FUNCTION_BLOCK OF";
85 | "DELETE";
86 | "GE";
87 | "ON";
88 | "DINT";
89 | "GT";
90 | "OR";
91 | "DIV";
92 | "IF";
93 | "OVERLAP";
94 | "DO";
95 | "IMPLEMENTS";
96 | "OVERRIDE";
97 | "DT";
98 | "INITIAL_STEP";
99 | "PRIORITY";
100 | "DWORD";
101 | "INSERT";
102 | "PRIVATE";
103 | "ELSE";
104 | "INT";
105 | "PROGRAM";
106 | "ELSIF";
107 | "INTERFACE";
108 | "PROTECTED";
109 | "END_ACTION";
110 | "INTERNAL";
111 | "PUBLIC";
112 | "END_CASE";
113 | "INTERVAL";
114 | "R_EDGE";
115 | "END_CLASS";
116 | "LD";
117 | "R_TRIG";
118 | "END_CONFIGURATION LDATE";
119 | "READ_ONLY";
120 | "END_FOR";
121 | "LDATE_AND_TIME READ_WRITE";
122 | "END_FUNCTION";
123 | "LDT";
124 | "REAL";
125 | "END_FUNCTION_BLOCK LE";
126 | "REF";
127 | "REF_TO";
128 | "REPEAT";
129 | "REPLACE";
130 | "RESOURCE";
131 | "RETAIN";
132 | "RETURN";
133 | "RIGHT";
134 | "ROL";
135 | "ROR";
136 | "RS";
137 | "SEL";
138 | "SHL";
139 | "SHR";
140 | "SIN";
141 | "SINGLE";
142 | "SINT";
143 | "SQRT";
144 | "SR";
145 | "STEP";
146 | "STRING";
147 | "STRING#";
148 | "STRUCT";
149 | "SUB";
150 | "SUPER";
151 | "T";
152 | "TAN";
153 | "TASK";
154 | "THEN";
155 | "THIS";
156 | "THIS";
157 | "TIME";
158 | "TIME_OF_DAY";
159 | "TO";
160 | "TOD";
161 | "TOF";
162 | "TON";
163 | "TP";
164 | "TRANSITION";
165 | "TRUE";
166 | "TRUNC";
167 | "TYPE";
168 | "UDINT";
169 | ]
170 |
171 | let check_name var =
172 | let name = S.VarUse.get_name var in
173 | let ti = S.VarUse.get_ti var in
174 | let m = List.find reserved_keywords ~f:(fun k -> String.equal name k) in
175 | match m with
176 | | Some _ ->
177 | let msg = "IEC data types and standard library objects must be avoided" in
178 | let w = Warn.mk ti.linenr ti.col "PLCOPEN-N3" msg in
179 | Some w
180 | | None -> None
181 |
182 | let do_check elems =
183 | let vardecls = List.fold_left
184 | elems
185 | ~init:[]
186 | ~f:(fun acc elem -> acc @ (AU.get_var_decls elem))
187 | in
188 | List.map
189 | vardecls
190 | ~f:(fun d -> begin
191 | let var = S.VarDecl.get_var d in
192 | check_name var
193 | end)
194 | |> List.filter ~f:(fun w -> match w with Some _ -> true | None -> false)
195 | |> List.map ~f:(fun w ->
196 | match w with Some w -> w | None -> assert false)
197 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IEC Checker
2 |
3 | > ⚠️ **Note:** Development paused. If you are interested in ICS security tooling and have funding, let's talk.
4 | >
5 | >📩 **Contact:** [oi@nowarp.io](mailto:oi@nowarp.io)
6 |
7 | This project aims to implement an open source tool for static code analysis of [IEC 61131-3](https://en.wikipedia.org/wiki/IEC_61131-3) programs.
8 |
9 | ## Supported languages
10 |
11 | This tool currently supports [Structured Text](https://en.wikipedia.org/wiki/Structured_text) programming language, [PLCOpen XML](https://plcopen.org/technical-activities/xml-exchange) and [SEL XML](https://selinc.com/products/3530/) formats.
12 | It works with extended Structured Text dialect that is completely compatible with [matiec](https://github.com/sm1820/matiec) transpiler.
13 |
14 | If you find, that `iec-checker` doesn't work with Structured Text extensions provided by your PLC vendor, please [let me know](https://github.com/jubnzv/iec-checker/issues). This can probably be easily implemented with some tweaks in the parser.
15 |
16 | ## Features
17 |
18 | The following features are currently implemented:
19 | + [PLCOpen Guidelines](https://plcopen.org/software-construction-guidelines) checks:
20 | - CP1: Access to a member shall be by name
21 | - CP2: All code shall be used in the application
22 | - CP3: All variables shall be initialized before being used
23 | - CP4: Direct addressing should not overlap
24 | - CP6: Avoid external variables in functions, function blocks and classes
25 | - CP8: Floating point comparison shall not be equality or inequality
26 | - CP9: Limit the complexity of POU code
27 | - CP13: POUs shall not call themselves directly or indirectly
28 | - CP25: Data type conversion should be explicit
29 | - CP28: Time and physical measures comparisons shall not be equality or inequality
30 | - L10: Usage of CONTINUE and EXIT instruction should be avoided
31 | - L17: Each IF instruction should have an ELSE clause
32 | - N3: Define the names to avoid
33 | + Declaration analysis for derived types
34 | + Intraprocedural control flow analysis: detection of unreachable code blocks inside the [POUs](https://en.wikipedia.org/wiki/IEC_61131-3#Program_organization_unit_(POU))
35 | + Detection of unused variables
36 | + Ability to integrate with other tools. Checker can dump its IR into a JSON file (`--dump` argument) and produce warnings in JSON format (`--output-format-format json`).
37 | + Can be extended with plugins written in Python. See demo plugin that plots the control flow graph: [cfg_plotter.py](./src/python/plugins/cfg_plotter.py).
38 |
39 | ## Installation
40 |
41 | You can download the latest binary release for Linux and Windows x86_64 from [GitHub releases](https://github.com/jubnzv/iec-checker/releases).
42 |
43 | ### Build from sources
44 |
45 | #### Linux
46 |
47 | Install the latest OCaml compiler and opam. Consider installation instructions at [ocaml.org](https://ocaml.org/docs/install.html) and [opam.ocaml.org](https://opam.ocaml.org/doc/Install.html).
48 |
49 | Then install the required dependencies:
50 |
51 | ```bash
52 | opam install --deps-only . # first time only
53 | ```
54 |
55 | Build and install the `bin/iec_checker` binary:
56 |
57 | ```bash
58 | make build
59 | ```
60 |
61 | #### Windows
62 |
63 | Install [OCaml for Windows](https://fdopen.github.io/opam-repository-mingw/) according to the [installation guide](https://fdopen.github.io/opam-repository-mingw/installation/). The graphic installer works well "out of the box".
64 |
65 | Then open installed Cygwin shell, clone the repository and use the installation instructions from the "Linux" section.
66 |
67 | ### Optional: Python scripts and test suite
68 | There is also a convenient [checker.py](./checker.py) script that wraps OCaml binary and provide additional options like extended formatting support and running the Python plugins. The test suite is also written in Python and requires a Python interpreter with some additional packages.
69 |
70 | Get [Python 3](https://www.python.org/downloads/) and install dependencies in the [virtual environment](https://docs.python.org/3/library/venv.html):
71 | ```bash
72 | virtualenv venv --python=/usr/bin/python3
73 | source venv/bin/activate
74 | pip3 install -r requirements.txt
75 | pip3 install -r requirements-dev.txt
76 | ```
77 |
78 | Then run unit tests:
79 | ```bash
80 | make test
81 | ```
82 |
83 | ## Usage examples
84 |
85 | Check some demo programs written in Structured Text:
86 |
87 | ```
88 | bin/iec_checker test/st/*.st
89 | ```
90 |
91 | You can also use `--help` argument to display help.
92 |
--------------------------------------------------------------------------------
/test/test_parser.py:
--------------------------------------------------------------------------------
1 | """Tests for parser and lexer."""
2 | import sys
3 | import os
4 |
5 | sys.path.append(os.path.join(os.path.dirname(
6 | os.path.abspath(__file__)), "../src"))
7 | from python.core import run_checker, check_program # noqa
8 | from python.dump import DumpManager # noqa
9 |
10 |
11 | def test_lexing_error():
12 | f = 'st/bad/lexing-error.st'
13 | fdump = f'{f}.dump.json'
14 | checker_warnings, rc = run_checker([f])
15 | assert rc == 1
16 | assert len(checker_warnings) == 1
17 | cv = checker_warnings[0]
18 | assert cv.id == 'LexingError'
19 | assert cv.linenr == 10
20 | assert cv.column == 6
21 | with DumpManager(fdump):
22 | pass
23 |
24 |
25 | def test_parser_errors():
26 | for fname in os.listdir('st/bad/'):
27 | f = os.path.join('st/bad/', fname)
28 | fdump = f'{f}.dump.json'
29 | checker_warnings, rc = run_checker([f])
30 | assert rc == 1, f"Incorrect exit code for {f}"
31 | assert len(checker_warnings) > 0
32 | with DumpManager(fdump):
33 | pass
34 |
35 |
36 | def test_no_parser_errors():
37 | for fname in os.listdir('st/good/'):
38 | if not fname.endswith('.st'):
39 | continue
40 | f = os.path.join('st/good/', fname)
41 | fdump = f'{f}.dump.json'
42 | checker_warnings, rc = run_checker([f])
43 | assert rc == 0, f"Incorrect exit code for {f}"
44 | with DumpManager(fdump):
45 | pass
46 |
47 |
48 | def test_direct_variables():
49 | f = 'st/good/direct-variables.st'
50 | fdump = f'{f}.dump.json'
51 | checker_warnings, rc = run_checker([f])
52 | assert rc == 0
53 | with DumpManager(fdump) as dm:
54 | _ = dm.scheme # TODO
55 |
56 |
57 | def test_statements_order():
58 | """Test that POU statements are arranged in the correct order."""
59 | fdump = f'stdin.dump.json'
60 | checker_warnings, rc = check_program(
61 | """
62 | PROGRAM p
63 | VAR a : INT; i : INT; END_VAR
64 | a := 1;
65 | i := 22;
66 | a := 16#42;
67 | END_PROGRAM
68 | """.replace('\n', ''))
69 | assert rc == 0
70 | with DumpManager(fdump) as dm:
71 | scheme = dm.scheme
72 | assert scheme
73 | assert len(scheme.programs) == 1
74 | # TODO: need recursive traverse in om
75 | # p = scheme.programs[0]
76 | # assert len(p.statemets) == 3
77 |
78 |
79 | def test_enum_types():
80 | fdump = f'stdin.dump.json'
81 | checker_warnings, rc = check_program(
82 | """
83 | TYPE
84 | Traffic_Light: (Red, Amber, Green);
85 | END_TYPE
86 | """.replace('\n', ''))
87 | assert rc == 0
88 | with DumpManager(fdump) as dm:
89 | scheme = dm.scheme
90 | assert scheme
91 | assert len(scheme.types) == 1
92 | ty = scheme.types[0]
93 | assert ty.name == 'TRAFFIC_LIGHT'
94 | assert ty.type == 'Enum'
95 |
96 |
97 | def test_struct_types():
98 | fdump = f'stdin.dump.json'
99 | checker_warnings, rc = check_program(
100 | """
101 | TYPE
102 | Cooler: STRUCT
103 | Temp: INT;
104 | Cooling: TOF;
105 | END_STRUCT;
106 | END_TYPE
107 | """.replace('\n', ''))
108 | assert rc == 0
109 | with DumpManager(fdump) as dm:
110 | scheme = dm.scheme
111 | assert scheme
112 | assert len(scheme.types) == 1
113 | ty = scheme.types[0]
114 | assert ty.name == 'COOLER'
115 | assert ty.type == 'Struct'
116 |
117 |
118 | def test_ref_types():
119 | fdump = f'stdin.dump.json'
120 | checker_warnings, rc = check_program(
121 | """
122 | TYPE myRef: REF_TO INT; END_TYPE
123 | """.replace('\n', ''))
124 | assert rc == 0
125 | with DumpManager(fdump) as dm:
126 | scheme = dm.scheme
127 | assert scheme
128 | assert len(scheme.types) == 1
129 | ty = scheme.types[0]
130 | assert ty.name == 'MYREF'
131 | assert ty.type == 'Ref'
132 |
133 |
134 | def test_array_types():
135 | fdump = f'stdin.dump.json'
136 | checker_warnings, rc = check_program(
137 | """
138 | TYPE BITS: ARRAY [0..7] OF BOOL; END_TYPE
139 | """.replace('\n', ''))
140 | assert rc == 0
141 | with DumpManager(fdump) as dm:
142 | scheme = dm.scheme
143 | assert scheme
144 | assert len(scheme.types) == 1
145 | ty = scheme.types[0]
146 | assert ty.name == 'BITS'
147 | assert ty.type == 'Array'
148 |
--------------------------------------------------------------------------------
/src/lib/plcopen_cp4.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 |
4 | module S = Syntax
5 | module AU = IECCheckerCore.Ast_util
6 |
7 | let get_ty_size = function
8 | | S.NIL -> 1
9 | | S.STRING len -> len
10 | | S.WSTRING len -> len * 2
11 | | S.CHAR len -> len
12 | | S.WCHAR len -> len * 2
13 | | S.TIME -> 8
14 | | S.LTIME -> 16
15 | | S.SINT -> 1
16 | | S.INT -> 2
17 | | S.DINT -> 4
18 | | S.LINT -> 8
19 | | S.USINT -> 1
20 | | S.UINT -> 2
21 | | S.UDINT -> 4
22 | | S.ULINT -> 8
23 | | S.REAL -> 4
24 | | S.LREAL -> 8
25 | | S.DATE -> 8
26 | | S.LDATE -> 16
27 | | S.TIME_OF_DAY -> 8
28 | | S.TOD -> 8
29 | | S.LTOD -> 16
30 | | S.DATE_AND_TIME -> 16
31 | | S.LDATE_AND_TIME -> 16
32 | | S.DT -> 8
33 | | S.LDT -> 16
34 | | S.BOOL -> 1
35 | | S.BYTE -> 1
36 | | S.WORD -> 2
37 | | S.DWORD -> 4
38 | | S.LWORD -> 8
39 |
40 | (** Find direct variable declared in this [elem] which address overlaps with [dirvar] with size [size]. *)
41 | (* FIXME: This is horrible slow to call it on each iteration. I have no idea how to implement the cache. *)
42 | let find_overlapping_var elem orig_dir_var size =
43 | (* get the path of the input dirvar *)
44 | let orig_path = S.DirVar.get_path orig_dir_var in
45 | if List.is_empty orig_path then
46 | None
47 | else begin
48 | let orig_last_path_num = List.reduce_exn ~f:(fun _ y -> y) orig_path in
49 | AU.get_var_decls elem
50 | |> List.fold_left
51 | ~init:None
52 | ~f:(fun acc var_decl -> begin
53 | match acc with
54 | | Some acc -> Some acc
55 | | None -> begin
56 | match (S.VarDecl.get_located_at var_decl) with
57 | | Some dir_var -> begin
58 | (* should have location *)
59 | match S.DirVar.get_loc dir_var with
60 | | Some _ -> begin
61 | (* should have path *)
62 | let path = S.DirVar.get_path dir_var in
63 | if (List.is_empty path ||
64 | not @@ phys_equal (List.length path) (List.length orig_path) ||
65 | not @@ List.equal Int.equal
66 | (List.take orig_path ((List.length orig_path) - 1))
67 | (List.take path ((List.length path) - 1))) then
68 | acc
69 | else begin
70 | (* compare paths *)
71 | let last_path_num = List.reduce_exn ~f:(fun _ y -> y) path in
72 | if (last_path_num < orig_last_path_num && last_path_num + size > orig_last_path_num) || (last_path_num > orig_last_path_num && last_path_num < orig_last_path_num + size) then Some dir_var
73 | else
74 | acc
75 | end
76 | end
77 | | None -> acc
78 | end
79 | | None -> acc
80 | end
81 | end)
82 | end
83 |
84 | let check_elem elem =
85 | AU.get_var_decls elem
86 | |> List.fold_left
87 | ~init:[]
88 | ~f:(fun acc decl -> begin
89 | match S.VarDecl.get_ty_spec decl with
90 | | Some S.DTyDeclSingleElement (elem_spec, _) -> begin
91 | match elem_spec with
92 | | S.DTySpecElementary elem_spec -> begin
93 | match S.VarDecl.get_located_at decl with
94 | | Some dir_var -> begin
95 | let size = get_ty_size elem_spec in
96 | match find_overlapping_var elem dir_var size with
97 | | Some overlapped_dir_var -> begin
98 | let ti = S.VarDecl.get_var_ti decl in
99 | let msg =
100 | Printf.sprintf("Address of direct variable %s (size %d) should not overlap with direct variable %s")
101 | (S.DirVar.get_name dir_var) size
102 | (S.DirVar.get_name overlapped_dir_var)
103 | in
104 | let w = Warn.mk ti.linenr ti.col "PLCOPEN-CP4" msg in
105 | acc @ [w];
106 | end
107 | | _ -> acc
108 | end
109 | | _ -> acc
110 | end
111 | | _ -> acc
112 | end
113 | | _ -> acc
114 | end)
115 |
116 | let do_check elems =
117 | List.fold_left elems ~init:[] ~f:(fun acc elem -> acc @ (check_elem elem))
118 |
--------------------------------------------------------------------------------
/src/analysis/use_define.ml:
--------------------------------------------------------------------------------
1 | open Core
2 |
3 | open IECCheckerCore
4 | module AU = Ast_util
5 | module S = Syntax
6 |
7 | (** Map that bounds declaration name of variable with S.VarDecl.t objects. *)
8 | module VarDeclMap = struct
9 | [@@@warning "-34"]
10 | [@@@warning "-32"]
11 | type t = (string, S.VarDecl.t, String.comparator_witness) Map.t
12 | let empty () = Map.empty (module String)
13 | let fold m = Map.fold m
14 | let find m name = Map.find m name
15 | let set m var_decl =
16 | Map.set m ~key:(S.VarDecl.get_var_name var_decl) ~data:(var_decl)
17 | let of_pou pou =
18 | AU.get_var_decls pou
19 | |> List.fold_left
20 | ~init:(empty ())
21 | ~f:(fun map var_decl -> set map var_decl)
22 | end
23 |
24 | (** Map that bounds variable name of variable with S.variable objects ("use"
25 | occurrence). *)
26 | module VarUseMap = struct
27 | [@@@warning "-34"]
28 | [@@@warning "-32"]
29 | type t = (string, S.VarUse.t list, String.comparator_witness) Map.t
30 | let empty () = Map.empty (module String)
31 | let fold m = Map.fold m
32 | let find m name = Map.find m name
33 | let add m var_use =
34 | let name = S.VarUse.get_name var_use in
35 | match find m name with
36 | | Some vs -> Map.set m ~key:name ~data:(vs @ [var_use])
37 | | None -> Map.set m ~key:name ~data:([var_use])
38 | let of_pou pou =
39 | AU.filter_exprs
40 | pou
41 | ~f:(fun expr -> begin
42 | match expr with S.ExprVariable _ -> true | _ -> false
43 | end)
44 | |> List.fold
45 | ~init:(empty ())
46 | ~f:(fun m expr -> begin
47 | match expr with
48 | | S.ExprVariable (_, v) -> add m v
49 | | _ -> assert false
50 | end)
51 | end
52 |
53 | (** Find errors when array variables addressed to index that exceeds defined
54 | array size. *)
55 | let check_array_out_of_bounds (decl_map : VarDeclMap.t) (use_map : VarUseMap.t) =
56 | let check_array_indexes (var_use : S.VarUse.t) (decl_subranges : S.arr_subrange list) : (Warn.t list) =
57 | let do_check idx_num (idx_value_opt : int option) =
58 | match List.nth decl_subranges idx_num with
59 | | Some sr -> begin
60 | match idx_value_opt with
61 | | Some idx_value -> begin
62 | if (idx_value < sr.arr_lower) || (idx_value > sr.arr_upper)then begin
63 | let ti = S.VarUse.get_ti var_use
64 | and name = S.VarUse.get_name var_use in
65 | let text =
66 | Printf.sprintf "%s index %d is out of range [%d .. %d]"
67 | name idx_value sr.arr_lower sr.arr_upper
68 | in
69 | [Warn.mk ti.linenr ti.col "OutOfBounds" text]
70 | end else []
71 | end
72 | | None (* opaque index *) -> []
73 | end
74 | | None -> begin
75 | let ti = S.VarUse.get_ti var_use
76 | and name = S.VarUse.get_name var_use in
77 | let text =
78 | Printf.sprintf "%s is addressed to %d dimension, but array was defined with %d dimensions"
79 | name (idx_num + 1) (List.length decl_subranges)
80 | in
81 | [Warn.mk ti.linenr ti.col "OutOfBounds" text]
82 | end
83 | in
84 | match S.VarUse.get_loc var_use with
85 | | S.VarUse.SymVar sv -> begin
86 | List.foldi
87 | (S.SymVar.get_array_indexes sv)
88 | ~init:[]
89 | ~f:(fun i acc_warns idx -> acc_warns @ (do_check i idx))
90 | end
91 | | S.VarUse.DirVar _ -> []
92 | in
93 | let check_var_use use_name var_use =
94 | match VarDeclMap.find decl_map use_name with
95 | | Some var_decl -> begin
96 | match S.VarDecl.get_ty_spec var_decl with
97 | | Some decl_spec -> begin
98 | match decl_spec with
99 | | S.DTyDeclArrayType (subranges, _, _) -> begin
100 | (check_array_indexes var_use subranges)
101 | end
102 | | S.DTyDeclSingleElement _ -> []
103 | | S.DTyDeclSubrange _ -> []
104 | | S.DTyDeclEnumType _ -> []
105 | | S.DTyDeclRefType _ -> []
106 | | S.DTyDeclStructType _ -> []
107 | end
108 | | None -> []
109 | end
110 | | None -> []
111 | in
112 | VarUseMap.fold
113 | use_map
114 | ~init:[]
115 | ~f:(fun ~key:use_name ~data:vars_use accm -> begin
116 | accm @ List.fold_left
117 | vars_use
118 | ~init:[]
119 | ~f:(fun accl var_use -> accl @ (check_var_use use_name var_use))
120 | end)
121 |
122 | let check_pou pou =
123 | let decl_map = VarDeclMap.of_pou pou in
124 | let use_map = VarUseMap.of_pou pou in
125 | List.rev (check_array_out_of_bounds decl_map use_map)
126 |
127 | let run elements =
128 | List.fold_left
129 | elements
130 | ~f:(fun warns e ->
131 | let ws = match e with
132 | | S.IECProgram _ | S.IECFunction _ | S.IECFunctionBlock _ -> check_pou e
133 | | _ -> []
134 | in
135 | warns @ ws)
136 | ~init:[]
137 |
--------------------------------------------------------------------------------
/test/selxml/SEL_RTAC/ProjSpace_MAIN_POU.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 32
6 | 3530
7 |
8 | ProjSpace_MAIN_POU
9 | FunctionBlock
10 |
11 |
46 | axisPosition);
54 |
55 | // Motor Halt
56 | IF mainMotor.IX_Halt THEN
57 | MC_jog(Axis:=X1,
58 | JogForward:=FALSE,
59 | JogBackward:=FALSE);
60 | END_IF
61 |
62 | // Main Power
63 | mainMotor.IX_Power:=TRUE;
64 | IF mainMotor.IX_Power THEN
65 | MC_power_X1(Axis:=X1,
66 | Enable:=TRUE,
67 | bRegulatorOn:=TRUE,
68 | bDriveStart:=TRUE);
69 | ELSE
70 | MC_power_X1(Axis:=X1,
71 | Enable:=FALSE,
72 | bRegulatorOn:=FALSE,
73 | bDriveStart:=FALSE);
74 | END_IF
75 |
76 | // STOP/RESET the motor
77 | MC_Stop_jog(Axis:=X1,
78 | Execute:=mainMotor.IX_Stop,
79 | Deceleration:=5);
80 | MC_Reset_jog(Axis:=X1,
81 | Execute:=mainMotor.IX_Reset);
82 |
83 | // Drive error ID
84 | gvl.jogErrorID := MC_jog.ErrorId;
85 |
86 | CASE state OF
87 | 0: // Power ON
88 | IF MC_power_X1.Status = TRUE THEN
89 | ton_0(IN:=NOT ton_0.Q,PT:=1);
90 | IF ton_0.Q THEN
91 | state := 10;
92 | END_IF
93 | END_IF
94 |
95 | 10:
96 | IF nextIntermediateTargetFloorAvailable=FALSE THEN
97 | FB5_Trigger_FB2_Call_FIFO_call(POP:=2);
98 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
99 | state:=11;
100 | END_IF
101 | ELSE
102 | state:=11;
103 | END_IF
104 |
105 | 11:
106 | IF gvl.Dout <>0 THEN
107 | state:=12;
108 | ELSE
109 | state:=10;
110 | END_IF
111 |
112 | 12:
113 | gvl.targetPosition := WORD_TO_INT(gvl.Dout-1)*gvl.floor_to_floor_distance;
114 | nextTargetFloor := WORD_TO_INT(gvl.Dout);
115 |
116 | IF gvl.Dout <> 0 AND gvl.targetPosition <> gvl.QW_axisPosition THEN
117 | IF gvl.QW_axisPosition < gvl.targetPosition THEN
118 | gvl.jogDirection:='GO_UP';
119 | MC_jog(Axis:=X1,
120 | JogForward:=TRUE,
121 | Velocity:=gvl.velocity_high,
122 | Acceleration:=gvl.acceleration_high,
123 | Deceleration:=gvl.deceleration_high);
124 | ELSIF gvl.QW_axisPosition > gvl.targetPosition THEN
125 | gvl.jogDirection:='GO_DOWN';
126 | MC_jog(Axis:=X1,
127 | JogBackward:=TRUE,
128 | Velocity:=gvl.velocity_high,
129 | Acceleration:=gvl.acceleration_high,
130 | Deceleration:=gvl.deceleration_high);
131 | END_IF
132 | ELSE
133 | state:=10;
134 | END_IF
135 |
136 | FB4_Populate_Intermediate_Call_List_call();
137 | IF gvl.jogDirection='GO_UP' THEN
138 | // If intermediate floor calls available_GO_UP
139 | FOR i:=1 TO gvl.NUM_FLOOR DO
140 | IF gvl.CALL_LIST_INTERMEDIATE_GO_UP[i]= TRUE AND gvl.QW_axisPosition > (i-1)*gvl.floor_to_floor_distance-200 THEN
141 | MC_jog(Axis:=X1,
142 | JogForward:=FALSE,
143 | JogBackward:=FALSE);
144 | nextIntermediateTargetFloor:=i;
145 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
146 | state:=100;
147 | END_IF
148 | END_FOR
149 | // If NO intermediate floor calls available
150 | IF gvl.QW_axisPosition > gvl.targetPosition-400 THEN
151 | MC_jog(Axis:=X1,
152 | JogForward:=FALSE,
153 | JogBackward:=FALSE);
154 | END_IF
155 | IF MC_jog.Busy=FALSE THEN
156 | state:=102;
157 | END_IF
158 | END_IF
159 |
160 | IF gvl.jogDirection='GO_DOWN' THEN
161 | // If intermediate floor calls available_GO_DOWN
162 | FOR i:=1 TO gvl.NUM_FLOOR DO
163 | IF gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[i]= TRUE AND gvl.QW_axisPosition < (i-1)*gvl.floor_to_floor_distance+200 THEN
164 | MC_jog(Axis:=X1,
165 | JogForward:=FALSE,
166 | JogBackward:=FALSE);
167 | nextIntermediateTargetFloor:=i;
168 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
169 | state:=100;
170 | END_IF
171 | END_FOR
172 | // If NO intermediate floor calls available
173 | IF gvl.QW_axisPosition < gvl.targetPosition+400 THEN
174 | MC_jog(Axis:=X1,
175 | JogForward:=FALSE,
176 | JogBackward:=FALSE);
177 | END_IF
178 | IF MC_jog.Busy=FALSE THEN
179 | state:=102;
180 | END_IF
181 | END_IF
182 |
183 |
184 | 100: // If YES intermediate floor calls available
185 | MC_MoveAbsolute_jog(Axis:=X1,
186 | Execute:=TRUE,
187 | Position:=gvl.nextIntermediateTargetPosition,
188 | Velocity:=gvl.velocity_low,
189 | Acceleration:=gvl.acceleration_low,
190 | Deceleration:=gvl.deceleration_low);
191 |
192 | IF MC_MoveAbsolute_jog.Done THEN
193 | MC_MoveAbsolute_jog(Axis:=X1,
194 | Execute:=FALSE);
195 | nextIntermediateTargetFloorAvailable:=TRUE;
196 | state:=400;
197 | END_IF;
198 |
199 | 102: // If NO intermediate floor calls available
200 | MC_MoveAbsolute_jog(Axis:=X1,
201 | Execute:=TRUE,
202 | Position:=gvl.targetPosition,
203 | Velocity:=gvl.velocity_low,
204 | Acceleration:=gvl.acceleration_low,
205 | Deceleration:=gvl.deceleration_low);
206 |
207 | IF MC_MoveAbsolute_jog.Done THEN
208 | MC_MoveAbsolute_jog(Axis:=X1,
209 | Execute:=FALSE);
210 | nextIntermediateTargetFloorAvailable:=FALSE;
211 | state:=400;
212 | END_IF
213 |
214 | 400: // Door closed and all safety OK.
215 | currentFloorNumber_:=FC1_currentFloorNumber(axisPosition:=axisPosition);
216 | gvl.Din_toRemove := INT_TO_WORD(currentFloorNumber_);
217 |
218 | gvl.CALL_LIST[currentFloorNumber_]:=FALSE;
219 | gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[currentFloorNumber_]:=FALSE;
220 | gvl.CALL_LIST_INTERMEDIATE_GO_UP[currentFloorNumber_]:=FALSE;
221 |
222 |
223 | FB5_Trigger_FB2_Call_FIFO_call(POP:=3);
224 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
225 | state:=402;
226 | END_IF
227 |
228 | 402:
229 | IF GVL.IX_DoorClosedAndAllSafetyOk THEN
230 | state:=10;
231 | END_IF
232 |
233 | END_CASE]]>
234 |
235 |
236 |
--------------------------------------------------------------------------------
/test/selxml/POUs/POUs Space/POUSpace_MAIN_POU.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | POUSpace_MAIN_POU
4 | FunctionBlock
5 |
40 | axisPosition);
48 |
49 | // Motor Halt
50 | IF mainMotor.IX_Halt THEN
51 | MC_jog(Axis:=X1,
52 | JogForward:=FALSE,
53 | JogBackward:=FALSE);
54 | END_IF
55 |
56 | // Main Power
57 | mainMotor.IX_Power:=TRUE;
58 | IF mainMotor.IX_Power THEN
59 | MC_power_X1(Axis:=X1,
60 | Enable:=TRUE,
61 | bRegulatorOn:=TRUE,
62 | bDriveStart:=TRUE);
63 | ELSE
64 | MC_power_X1(Axis:=X1,
65 | Enable:=FALSE,
66 | bRegulatorOn:=FALSE,
67 | bDriveStart:=FALSE);
68 | END_IF
69 |
70 | // STOP/RESET the motor
71 | MC_Stop_jog(Axis:=X1,
72 | Execute:=mainMotor.IX_Stop,
73 | Deceleration:=5);
74 | MC_Reset_jog(Axis:=X1,
75 | Execute:=mainMotor.IX_Reset);
76 |
77 | // Drive error ID
78 | gvl.jogErrorID := MC_jog.ErrorId;
79 |
80 | CASE state OF
81 | 0: // Power ON
82 | IF MC_power_X1.Status = TRUE THEN
83 | ton_0(IN:=NOT ton_0.Q,PT:=1);
84 | IF ton_0.Q THEN
85 | state := 10;
86 | END_IF
87 | END_IF
88 |
89 | 10:
90 | IF nextIntermediateTargetFloorAvailable=FALSE THEN
91 | FB5_Trigger_FB2_Call_FIFO_call(POP:=2);
92 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
93 | state:=11;
94 | END_IF
95 | ELSE
96 | state:=11;
97 | END_IF
98 |
99 | 11:
100 | IF gvl.Dout <>0 THEN
101 | state:=12;
102 | ELSE
103 | state:=10;
104 | END_IF
105 |
106 | 12:
107 | gvl.targetPosition := WORD_TO_INT(gvl.Dout-1)*gvl.floor_to_floor_distance;
108 | nextTargetFloor := WORD_TO_INT(gvl.Dout);
109 |
110 | IF gvl.Dout <> 0 AND gvl.targetPosition <> gvl.QW_axisPosition THEN
111 | IF gvl.QW_axisPosition < gvl.targetPosition THEN
112 | gvl.jogDirection:='GO_UP';
113 | MC_jog(Axis:=X1,
114 | JogForward:=TRUE,
115 | Velocity:=gvl.velocity_high,
116 | Acceleration:=gvl.acceleration_high,
117 | Deceleration:=gvl.deceleration_high);
118 | ELSIF gvl.QW_axisPosition > gvl.targetPosition THEN
119 | gvl.jogDirection:='GO_DOWN';
120 | MC_jog(Axis:=X1,
121 | JogBackward:=TRUE,
122 | Velocity:=gvl.velocity_high,
123 | Acceleration:=gvl.acceleration_high,
124 | Deceleration:=gvl.deceleration_high);
125 | END_IF
126 | ELSE
127 | state:=10;
128 | END_IF
129 |
130 | FB4_Populate_Intermediate_Call_List_call();
131 | IF gvl.jogDirection='GO_UP' THEN
132 | // If intermediate floor calls available_GO_UP
133 | FOR i:=1 TO gvl.NUM_FLOOR DO
134 | IF gvl.CALL_LIST_INTERMEDIATE_GO_UP[i]= TRUE AND gvl.QW_axisPosition > (i-1)*gvl.floor_to_floor_distance-200 THEN
135 | MC_jog(Axis:=X1,
136 | JogForward:=FALSE,
137 | JogBackward:=FALSE);
138 | nextIntermediateTargetFloor:=i;
139 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
140 | state:=100;
141 | END_IF
142 | END_FOR
143 | // If NO intermediate floor calls available
144 | IF gvl.QW_axisPosition > gvl.targetPosition-400 THEN
145 | MC_jog(Axis:=X1,
146 | JogForward:=FALSE,
147 | JogBackward:=FALSE);
148 | END_IF
149 | IF MC_jog.Busy=FALSE THEN
150 | state:=102;
151 | END_IF
152 | END_IF
153 |
154 | IF gvl.jogDirection='GO_DOWN' THEN
155 | // If intermediate floor calls available_GO_DOWN
156 | FOR i:=1 TO gvl.NUM_FLOOR DO
157 | IF gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[i]= TRUE AND gvl.QW_axisPosition < (i-1)*gvl.floor_to_floor_distance+200 THEN
158 | MC_jog(Axis:=X1,
159 | JogForward:=FALSE,
160 | JogBackward:=FALSE);
161 | nextIntermediateTargetFloor:=i;
162 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
163 | state:=100;
164 | END_IF
165 | END_FOR
166 | // If NO intermediate floor calls available
167 | IF gvl.QW_axisPosition < gvl.targetPosition+400 THEN
168 | MC_jog(Axis:=X1,
169 | JogForward:=FALSE,
170 | JogBackward:=FALSE);
171 | END_IF
172 | IF MC_jog.Busy=FALSE THEN
173 | state:=102;
174 | END_IF
175 | END_IF
176 |
177 |
178 | 100: // If YES intermediate floor calls available
179 | MC_MoveAbsolute_jog(Axis:=X1,
180 | Execute:=TRUE,
181 | Position:=gvl.nextIntermediateTargetPosition,
182 | Velocity:=gvl.velocity_low,
183 | Acceleration:=gvl.acceleration_low,
184 | Deceleration:=gvl.deceleration_low);
185 |
186 | IF MC_MoveAbsolute_jog.Done THEN
187 | MC_MoveAbsolute_jog(Axis:=X1,
188 | Execute:=FALSE);
189 | nextIntermediateTargetFloorAvailable:=TRUE;
190 | state:=400;
191 | END_IF;
192 |
193 | 102: // If NO intermediate floor calls available
194 | MC_MoveAbsolute_jog(Axis:=X1,
195 | Execute:=TRUE,
196 | Position:=gvl.targetPosition,
197 | Velocity:=gvl.velocity_low,
198 | Acceleration:=gvl.acceleration_low,
199 | Deceleration:=gvl.deceleration_low);
200 |
201 | IF MC_MoveAbsolute_jog.Done THEN
202 | MC_MoveAbsolute_jog(Axis:=X1,
203 | Execute:=FALSE);
204 | nextIntermediateTargetFloorAvailable:=FALSE;
205 | state:=400;
206 | END_IF
207 |
208 | 400: // Door closed and all safety OK.
209 | currentFloorNumber_:=FC1_currentFloorNumber(axisPosition:=axisPosition);
210 | gvl.Din_toRemove := INT_TO_WORD(currentFloorNumber_);
211 |
212 | gvl.CALL_LIST[currentFloorNumber_]:=FALSE;
213 | gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[currentFloorNumber_]:=FALSE;
214 | gvl.CALL_LIST_INTERMEDIATE_GO_UP[currentFloorNumber_]:=FALSE;
215 |
216 |
217 | FB5_Trigger_FB2_Call_FIFO_call(POP:=3);
218 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
219 | state:=402;
220 | END_IF
221 |
222 | 402:
223 | IF GVL.IX_DoorClosedAndAllSafetyOk THEN
224 | state:=10;
225 | END_IF
226 |
227 | END_CASE]]>
228 |
229 |
230 | 6f9dac99-8de1-4efc-8465-68ac443b7d08
231 | ]]>
232 |
233 |
--------------------------------------------------------------------------------
/src/python/om.py:
--------------------------------------------------------------------------------
1 | """Object model
2 | Object model represents the IR data from collected from the ``iec-checker``
3 | output.
4 | """
5 | from dataclasses import dataclass
6 | from typing import List, Dict, Set
7 |
8 |
9 | @dataclass
10 | class Tok_info:
11 | id: int
12 | linenr: int
13 | col: int
14 |
15 | @classmethod
16 | def from_dict(cls, values):
17 | args = {}
18 | args['id'] = values.get('id', -1)
19 | args['linenr'] = values.get('linenr', -1)
20 | args['col'] = values.get('col', -1)
21 | return Tok_info(**args)
22 |
23 |
24 | @dataclass
25 | class Statement:
26 | ty: str
27 | nested: List
28 |
29 | def detect_ty(values):
30 | if not isinstance(values, list):
31 | return 'Opaque'
32 | if len(values) == 0:
33 | return 'Opaque'
34 | return values[0]
35 |
36 | def get_nested(ty, values):
37 | if len(values) < 2:
38 | return []
39 | if ty == 'While':
40 | return Statement.from_dict(values[1])
41 | return []
42 |
43 | @classmethod
44 | def from_dict(cls, values):
45 | args = {}
46 | args['ty'] = cls.detect_ty(values)
47 | args['nested'] = cls.get_nested(args['ty'], values)
48 |
49 | return Statement(**args)
50 |
51 |
52 | @dataclass
53 | class Variable:
54 | name: str
55 |
56 | @classmethod
57 | def from_dict(cls, values):
58 | pass
59 |
60 |
61 | @dataclass
62 | class VarDecl:
63 | var: Variable
64 | # spec:
65 | # qual
66 | # dir
67 |
68 | @classmethod
69 | def from_dict(cls, values):
70 | pass
71 |
72 |
73 | @dataclass
74 | class Function:
75 | name: str
76 | ti: Tok_info
77 | is_std: bool
78 | # return_ty:
79 | variables: List[VarDecl]
80 | statements: List[Statement]
81 |
82 | @classmethod
83 | def from_dict(cls, values):
84 | args = {}
85 |
86 | id_ = values.get('id')
87 | if id_:
88 | args['name'] = id_.get('name', '')
89 | args['ti'] = Tok_info.from_dict(id_.get('ti'))
90 | args['is_std'] = id_.get('is_std', False)
91 | else:
92 | args['name'] = ''
93 | args['ti'] = None
94 | args['is_std'] = False
95 |
96 | args['variables'] = [Variable.from_dict(
97 | i) for i in values.get('variables', [])]
98 | args['statements'] = [Statement.from_dict(
99 | i) for i in values.get('statements', [])]
100 | return Function(**args)
101 |
102 |
103 | @dataclass
104 | class FunctionBlock:
105 | name: str
106 | ti: Tok_info
107 | is_std: bool
108 | variables: List[VarDecl]
109 | statements: List[Statement]
110 |
111 | @classmethod
112 | def from_dict(cls, values):
113 | args = {}
114 |
115 | id_ = values.get('id')
116 | if id_:
117 | args['name'] = id_.get('name', '')
118 | args['ti'] = Tok_info.from_dict(id_.get('ti'))
119 | args['is_std'] = id_.get('is_std', False)
120 | else:
121 | args['name'] = ''
122 | args['ti'] = None
123 | args['is_std'] = False
124 |
125 | args['variables'] = [Variable.from_dict(
126 | i) for i in values.get('variables', [])]
127 | args['statements'] = [Statement.from_dict(
128 | i) for i in values.get('statements', [])]
129 | return FunctionBlock(**args)
130 |
131 |
132 | @dataclass
133 | class Program:
134 | name: str
135 | is_retain: bool
136 | variables: List[VarDecl]
137 | statements: List[Statement]
138 |
139 | @classmethod
140 | def from_dict(cls, values):
141 | args = {}
142 | args['name'] = values.get('name', '')
143 | args['is_retain'] = values.get('is_retain', False)
144 | args['variables'] = [Variable.from_dict(
145 | i) for i in values.get('variables', [])]
146 | args['statements'] = [Statement.from_dict(
147 | i) for i in values.get('statements', [])]
148 | return Program(**args)
149 |
150 |
151 | @dataclass
152 | class Configuration:
153 | name: str
154 |
155 |
156 | @dataclass
157 | class Type:
158 | name: str
159 | type: str
160 |
161 | @classmethod
162 | def from_dict(cls, values):
163 | args = {}
164 | args['name'] = values.get('name', '')
165 | args['type'] = values.get('type', '')
166 | return Type(**args)
167 |
168 |
169 | @dataclass
170 | class Environment:
171 | name: str
172 |
173 |
174 | @dataclass
175 | class BasicBlock:
176 | """Basic block of intraprocedural control flow graph."""
177 | id: int
178 | type: str
179 | preds: Set[int]
180 | succs: Set[int]
181 | stmt_ids: List[int]
182 |
183 | @classmethod
184 | def from_dict(cls, values):
185 | args = {}
186 | args['id'] = values.get('id', -1)
187 | args['type'] = values.get('type', [])
188 | if len(args['type']) > 0:
189 | args['type'] = args['type'][0]
190 | args['preds'] = set(values.get('preds', []))
191 | args['succs'] = set(values.get('succs', []))
192 | args['stmt_ids'] = values.get('stmt_ids', [])
193 | return BasicBlock(**args)
194 |
195 |
196 | @dataclass
197 | class Cfg:
198 | """Intraprocedural control flow graph."""
199 | entry_bb_id: int
200 | basic_blocks: List[BasicBlock]
201 | pou_id: int
202 |
203 | @classmethod
204 | def from_dict(cls, values):
205 | args = {}
206 | args['entry_bb_id'] = values.get('entry_bb_id', -1)
207 | args['basic_blocks'] = [BasicBlock.from_dict(
208 | bb) for bb in values.get('basic_blocks')]
209 | args['pou_id'] = values.get('pou_id', -1)
210 | return Cfg(**args)
211 |
212 |
213 | @dataclass
214 | class Scheme:
215 | version: str
216 | functions: List[Function]
217 | function_blocks: List[FunctionBlock]
218 | programs: List[Program]
219 | configurations: List[Configuration]
220 | types: List[Type]
221 | environments: List[Environment]
222 | cfgs: List[Cfg]
223 |
224 | @classmethod
225 | def from_dict(cls, values: Dict):
226 | args = {}
227 | args['version'] = values.get('version', '0')
228 | args['functions'] = [Function.from_dict(
229 | i) for i in values.get('functions', [])]
230 | args['function_blocks'] = [FunctionBlock.from_dict(
231 | i) for i in values.get('function_blocks', [])]
232 | args['programs'] = [Program.from_dict(
233 | i) for i in values.get('programs', [])]
234 | # args['configurations'] = [Configuration.from_dict(
235 | # i) for i in values.get('configurations', [])]
236 | args['configurations'] = []
237 | args['types'] = [Type.from_dict(i) for i in values.get('types', [])]
238 | # args['environments'] = [Environment.from_dict(
239 | # i) for i in values.get('environments', [])]
240 | args['environments'] = []
241 | args['cfgs'] = [Cfg.from_dict(i) for i in values.get('cfgs', [])]
242 | return Scheme(**args)
243 |
244 |
245 | @dataclass
246 | class Warning:
247 | """Warning found by OCaml core."""
248 | linenr: int
249 | column: int
250 | id: str
251 | msg: str
252 | type: str
253 |
254 | @classmethod
255 | def from_dict(cls, values):
256 | args = {}
257 | args['linenr'] = values.get('linenr', -1)
258 | args['column'] = values.get('column', -1)
259 | args['id'] = values.get('id', -1)
260 | args['msg'] = values.get('msg', '')
261 | args['type'] = values.get('type', 'Inspection')
262 | return Warning(**args)
263 |
264 | def __str__(self):
265 | if self.linenr == 0 and self.column == 0:
266 | return f"[{self.id}] {self.msg}"
267 | return f"[{self.id}] {self.linenr}:{self.column} {self.msg}"
268 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/test/st/good/user3.st:
--------------------------------------------------------------------------------
1 | FUNCTION_BLOCK MAIN_POU
2 |
3 | VAR_INPUT
4 | END_VAR
5 |
6 | VAR_OUTPUT
7 | END_VAR
8 |
9 | VAR
10 | FB4_Populate_Intermediate_Call_List_call : FB4_Populate_Intermediate_Call_List;
11 | FB5_Trigger_FB2_Call_FIFO_call:FB5_Trigger_FB2_Call_FIFO;
12 | MC_power_X1 : MC_Power;
13 | MC_readAxisPosition_jog:MC_ReadActualPosition;
14 | MC_ReadStatus_jog:MC_ReadStatus;
15 | MC_jog:MC_Jog;
16 | MC_Halt_jog:MC_Halt;
17 | MC_Stop_jog:MC_Stop;
18 | MC_Reset_jog:MC_Reset;
19 | MC_MoveAbsolute_jog:MC_MoveAbsolute;
20 | mainMotor:Motor;
21 | ton_0 : TON;
22 | ton_1 : TON;
23 | ton_2 : TON;
24 | axisPosition:REAL;
25 | nextTargetFloor:INT;
26 | nextTargetPosition:REAL;
27 | nextIntermediateTargetPosition:REAL;
28 | nextIntermediateTargetFloor:INT;
29 | nextIntermediateTargetFloorAvailable:bool;
30 | lastTargetFloor:INT;
31 | currentFloorNumber_:INT;
32 | i:INT;
33 | state: INT:=0;
34 | test_READ:BOOL;
35 | test_FORWARD:BOOL;
36 | END_VAR
37 |
38 | // Write values to Global Variables
39 | gvl.state_POU_1:=state;
40 | gvl.QW_axisPosition:=axisPosition;
41 |
42 | // Read axis position all the time
43 | MC_readAxisPosition_jog(Axis:=X1,
44 | Enable:=TRUE,
45 | Position=>axisPosition);
46 |
47 | // Motor Halt
48 | IF mainMotor.IX_Halt THEN
49 | MC_jog(Axis:=X1,
50 | JogForward:=FALSE,
51 | JogBackward:=FALSE);
52 | END_IF
53 |
54 | // Main Power
55 | mainMotor.IX_Power:=TRUE;
56 | IF mainMotor.IX_Power THEN
57 | MC_power_X1(Axis:=X1,
58 | Enable:=TRUE,
59 | bRegulatorOn:=TRUE,
60 | bDriveStart:=TRUE);
61 | ELSE
62 | MC_power_X1(Axis:=X1,
63 | Enable:=FALSE,
64 | bRegulatorOn:=FALSE,
65 | bDriveStart:=FALSE);
66 | END_IF
67 |
68 | // STOP/RESET the motor
69 | MC_Stop_jog(Axis:=X1,
70 | Execute:=mainMotor.IX_Stop,
71 | Deceleration:=5);
72 | MC_Reset_jog(Axis:=X1,
73 | Execute:=mainMotor.IX_Reset);
74 |
75 | // Drive error ID
76 | gvl.jogErrorID := MC_jog.ErrorId;
77 |
78 | CASE state OF
79 | 0: // Power ON
80 | IF MC_power_X1.Status = TRUE THEN
81 | ton_0(IN:=NOT ton_0.Q,PT:=1);
82 | IF ton_0.Q THEN
83 | state := 10;
84 | END_IF
85 | END_IF
86 |
87 | 10:
88 | IF nextIntermediateTargetFloorAvailable=FALSE THEN
89 | FB5_Trigger_FB2_Call_FIFO_call(POP:=2);
90 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
91 | state:=11;
92 | END_IF
93 | ELSE
94 | state:=11;
95 | END_IF
96 |
97 | 11:
98 | IF gvl.Dout <>0 THEN
99 | state:=12;
100 | ELSE
101 | state:=10;
102 | END_IF
103 |
104 | 12:
105 | gvl.targetPosition := WORD_TO_INT(gvl.Dout-1)*gvl.floor_to_floor_distance;
106 | nextTargetFloor := WORD_TO_INT(gvl.Dout);
107 |
108 | IF gvl.Dout <> 0 AND gvl.targetPosition <> gvl.QW_axisPosition THEN
109 | IF gvl.QW_axisPosition < gvl.targetPosition THEN
110 | gvl.jogDirection:='GO_UP';
111 | MC_jog(Axis:=X1,
112 | JogForward:=TRUE,
113 | Velocity:=gvl.velocity_high,
114 | Acceleration:=gvl.acceleration_high,
115 | Deceleration:=gvl.deceleration_high);
116 | ELSIF gvl.QW_axisPosition > gvl.targetPosition THEN
117 | gvl.jogDirection:='GO_DOWN';
118 | MC_jog(Axis:=X1,
119 | JogBackward:=TRUE,
120 | Velocity:=gvl.velocity_high,
121 | Acceleration:=gvl.acceleration_high,
122 | Deceleration:=gvl.deceleration_high);
123 | END_IF
124 | ELSE
125 | state:=10;
126 | END_IF
127 |
128 | FB4_Populate_Intermediate_Call_List_call();
129 | IF gvl.jogDirection='GO_UP' THEN
130 | // If intermediate floor calls available_GO_UP
131 | FOR i:=1 TO gvl.NUM_FLOOR DO
132 | IF gvl.CALL_LIST_INTERMEDIATE_GO_UP[i]= TRUE AND gvl.QW_axisPosition > (i-1)*gvl.floor_to_floor_distance-200 THEN
133 | MC_jog(Axis:=X1,
134 | JogForward:=FALSE,
135 | JogBackward:=FALSE);
136 | nextIntermediateTargetFloor:=i;
137 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
138 | state:=100;
139 | END_IF
140 | END_FOR
141 | // If NO intermediate floor calls available
142 | IF gvl.QW_axisPosition > gvl.targetPosition-400 THEN
143 | MC_jog(Axis:=X1,
144 | JogForward:=FALSE,
145 | JogBackward:=FALSE);
146 | END_IF
147 | IF MC_jog.Busy=FALSE THEN
148 | state:=102;
149 | END_IF
150 | END_IF
151 |
152 | IF gvl.jogDirection='GO_DOWN' THEN
153 | // If intermediate floor calls available_GO_DOWN
154 | FOR i:=1 TO gvl.NUM_FLOOR DO
155 | IF gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[i]= TRUE AND gvl.QW_axisPosition < (i-1)*gvl.floor_to_floor_distance+200 THEN
156 | MC_jog(Axis:=X1,
157 | JogForward:=FALSE,
158 | JogBackward:=FALSE);
159 | nextIntermediateTargetFloor:=i;
160 | gvl.nextIntermediateTargetPosition:=(i-1)*gvl.floor_to_floor_distance;
161 | state:=100;
162 | END_IF
163 | END_FOR
164 | // If NO intermediate floor calls available
165 | IF gvl.QW_axisPosition < gvl.targetPosition+400 THEN
166 | MC_jog(Axis:=X1,
167 | JogForward:=FALSE,
168 | JogBackward:=FALSE);
169 | END_IF
170 | IF MC_jog.Busy=FALSE THEN
171 | state:=102;
172 | END_IF
173 | END_IF
174 |
175 |
176 | 100: // If YES intermediate floor calls available
177 | MC_MoveAbsolute_jog(Axis:=X1,
178 | Execute:=TRUE,
179 | Position:=gvl.nextIntermediateTargetPosition,
180 | Velocity:=gvl.velocity_low,
181 | Acceleration:=gvl.acceleration_low,
182 | Deceleration:=gvl.deceleration_low);
183 |
184 | IF MC_MoveAbsolute_jog.Done THEN
185 | MC_MoveAbsolute_jog(Axis:=X1,
186 | Execute:=FALSE);
187 | nextIntermediateTargetFloorAvailable:=TRUE;
188 | state:=400;
189 | END_IF;
190 |
191 | 102: // If NO intermediate floor calls available
192 | MC_MoveAbsolute_jog(Axis:=X1,
193 | Execute:=TRUE,
194 | Position:=gvl.targetPosition,
195 | Velocity:=gvl.velocity_low,
196 | Acceleration:=gvl.acceleration_low,
197 | Deceleration:=gvl.deceleration_low);
198 |
199 | IF MC_MoveAbsolute_jog.Done THEN
200 | MC_MoveAbsolute_jog(Axis:=X1,
201 | Execute:=FALSE);
202 | nextIntermediateTargetFloorAvailable:=FALSE;
203 | state:=400;
204 | END_IF
205 |
206 | 400: // Door closed and all safety OK.
207 | currentFloorNumber_:=FC1_currentFloorNumber(axisPosition:=axisPosition);
208 | gvl.Din_toRemove := INT_TO_WORD(currentFloorNumber_);
209 |
210 | gvl.CALL_LIST[currentFloorNumber_]:=FALSE;
211 | gvl.CALL_LIST_INTERMEDIATE_GO_DOWN[currentFloorNumber_]:=FALSE;
212 | gvl.CALL_LIST_INTERMEDIATE_GO_UP[currentFloorNumber_]:=FALSE;
213 |
214 |
215 | FB5_Trigger_FB2_Call_FIFO_call(POP:=3);
216 | IF FB5_Trigger_FB2_Call_FIFO_call.done = TRUE THEN
217 | state:=402;
218 | END_IF
219 |
220 | 402:
221 | IF GVL.IX_DoorClosedAndAllSafetyOk THEN
222 | state:=10;
223 | END_IF
224 |
225 | END_CASE
226 |
227 | END_FUNCTION_BLOCK
228 |
--------------------------------------------------------------------------------
/src/core/ast_util.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | module S = Syntax
3 | module TI = Tok_info
4 |
5 | let get_var_decls = function
6 | | S.IECFunction (_, f) -> f.variables
7 | | S.IECFunctionBlock (_, fb) -> fb.variables
8 | | S.IECProgram (_, p) -> p.variables
9 | | S.IECClass (_, c) -> c.variables
10 | | S.IECInterface _ -> []
11 | | S.IECConfiguration (_, c) -> c.variables
12 | | S.IECType _ -> []
13 |
14 | let expr_to_stmts expr : S.statement list =
15 | let rec aux = function
16 | | S.ExprVariable _ -> []
17 | | S.ExprConstant _ -> []
18 | | S.ExprBin (_, e1, _, e2) -> aux e1 @ aux e2
19 | | S.ExprUn (_, _, e) -> aux e
20 | | S.ExprFuncCall (_, s) -> [s]
21 | in
22 | aux expr
23 |
24 | let rec stmts_to_list stmt =
25 | let get_nested stmts =
26 | List.fold_left
27 | stmts
28 | ~init:[]
29 | ~f:(fun acc s -> acc @ (stmts_to_list s))
30 | in
31 | match stmt with
32 | | S.StmExpr (_, e) -> [stmt] @ expr_to_stmts e
33 | | S.StmElsif (_, cond_stmts, body_stmts) ->
34 | [ stmt ] @ stmts_to_list cond_stmts
35 | @ List.fold_left
36 | body_stmts
37 | ~init:[]
38 | ~f:(fun acc s -> acc @ stmts_to_list s)
39 | | S.StmIf (_, cond_s, body_ss, elsif_ss, else_ss) ->
40 | [ stmt ]
41 | @ List.fold_left
42 | ([cond_s] @ body_ss @ elsif_ss @ else_ss)
43 | ~f:(fun ss s -> ss @ stmts_to_list s)
44 | ~init:[]
45 | | S.StmCase (_, cond_s, case_sels, else_ss) ->
46 | let case_stmts =
47 | List.fold_left
48 | case_sels
49 | ~init:[]
50 | ~f:(fun acc cs -> acc @ (get_nested cs.case) @ (get_nested cs.body))
51 | in
52 | [cond_s] @ case_stmts @ (get_nested else_ss)
53 | | S.StmFor (_, ctrl, body_stmts) ->
54 | [ ctrl.assign ] @
55 | List.fold_left body_stmts ~init:[] ~f:(fun acc s -> acc @ stmts_to_list s)
56 | | S.StmWhile (_, cond_stmt, ns) ->
57 | [stmt] @ [cond_stmt] @
58 | List.fold_left ns ~f:(fun ss s -> ss @ stmts_to_list s) ~init:[]
59 | | S.StmRepeat (_, body_stmts, cond_stmt) ->
60 | [stmt] @
61 | List.fold_left body_stmts
62 | ~init:[]
63 | ~f:(fun ss s -> ss @ stmts_to_list s) @
64 | [cond_stmt]
65 | | S.StmExit _ -> [ stmt ]
66 | | S.StmContinue _ -> [ stmt ]
67 | | S.StmReturn _ -> [ stmt ]
68 | | S.StmFuncCall (_, _, func_params) -> begin
69 | let func_params_stmts = List.fold_left
70 | func_params
71 | ~init:[]
72 | ~f:(fun acc fp -> acc @ [fp.stmt])
73 | in
74 | [stmt] @ func_params_stmts
75 | end
76 |
77 | let get_pou_stmts = function
78 | | S.IECFunction (_, f) ->
79 | List.fold_left f.statements ~f:(fun ss s -> ss @ stmts_to_list s) ~init:[]
80 | | S.IECFunctionBlock (_, fb) ->
81 | List.fold_left fb.statements
82 | ~f:(fun ss s -> ss @ stmts_to_list s)
83 | ~init:[]
84 | | S.IECProgram (_, p) ->
85 | List.fold_left p.statements ~f:(fun ss s -> ss @ stmts_to_list s) ~init:[]
86 | | S.IECClass (_, c) ->
87 | List.fold_left
88 | c.methods
89 | ~init:[]
90 | ~f:(fun acc m -> acc @ List.fold_left m.statements ~init:[] ~f:(fun acc s -> acc @ stmts_to_list s))
91 | | S.IECInterface _ -> []
92 | | S.IECConfiguration _ -> []
93 | | S.IECType _ -> []
94 |
95 | let get_top_stmts = function
96 | | S.IECFunction (_, f) -> f.statements
97 | | S.IECFunctionBlock (_, fb) -> fb.statements
98 | | S.IECProgram (_, p) -> p.statements
99 | | S.IECClass (_, c) -> List.fold_left c.methods ~init:[] ~f:(fun acc m -> acc @ m.statements)
100 | | S.IECInterface _ -> []
101 | | S.IECConfiguration _ -> []
102 | | S.IECType _ -> []
103 |
104 | let get_stmts_num elem =
105 | List.length (get_pou_stmts elem)
106 |
107 | let get_stmts elems =
108 | List.fold_left elems
109 | ~f:(fun x e ->
110 | let es = get_pou_stmts e in
111 | x @ es)
112 | ~init:[]
113 |
114 | let rec get_stmt_exprs stmt =
115 | let get_nested stmts =
116 | List.fold_left stmts ~init:[] ~f:(fun acc es -> acc @ (get_stmt_exprs es))
117 | in
118 | match stmt with
119 | | S.StmExpr (_, e) -> [e]
120 | | S.StmElsif (_, cond_s, ss) -> (get_nested [cond_s]) @ (get_nested ss)
121 | | S.StmIf (_, cond_s, body_ss, elsif_ss, else_ss) -> (
122 | (get_nested [cond_s]) @
123 | (get_nested body_ss) @
124 | (get_nested elsif_ss) @
125 | (get_nested else_ss)
126 | )
127 | | S.StmCase (_, cond_s, case_sels, else_ss) ->
128 | begin
129 | let case_stmts =
130 | List.fold_left
131 | case_sels
132 | ~init:[]
133 | ~f:(fun acc case_sel -> acc @ (get_nested case_sel.case) @ (get_nested case_sel.body))
134 | in
135 | (get_nested [cond_s]) @
136 | case_stmts @
137 | (get_nested else_ss)
138 | end
139 | | S.StmFor (_, ctrl, body_stmts) -> (
140 | (get_nested [ctrl.assign]) @
141 | [ctrl.range_end; ctrl.range_step] @
142 | (get_nested body_stmts)
143 | )
144 | | S.StmWhile (_, cond_stmt, ss) -> (get_nested [cond_stmt]) @ (get_nested ss)
145 | | S.StmRepeat (_, body_stmts, cond_stmt) -> (get_nested body_stmts) @ (get_nested [cond_stmt])
146 | | S.StmFuncCall (_, _, func_params) -> begin
147 | let func_params_stmts = List.fold_left
148 | func_params
149 | ~init:[]
150 | ~f:(fun acc fp -> acc @ [fp.stmt])
151 | in
152 | (get_nested func_params_stmts)
153 | end
154 | | S.StmExit _ | S.StmContinue _ | S.StmReturn _ -> []
155 |
156 | let get_pou_exprs elem =
157 | get_pou_stmts elem
158 | |> List.fold_left ~init:[] ~f:(fun acc stmt -> acc @ (get_stmt_exprs stmt))
159 |
160 | let get_var_uses elem =
161 | let rec get_vars = function
162 | | S.ExprVariable (_, vu) -> [vu]
163 | | S.ExprConstant _ -> []
164 | | S.ExprBin (_, lhs, _, rhs) -> (get_vars lhs) @ (get_vars rhs)
165 | | S.ExprUn (_, _, e) -> get_vars e
166 | | S.ExprFuncCall (_, stmt) -> begin
167 | get_stmt_exprs stmt
168 | |> List.fold_left ~init:[] ~f:(fun acc e -> acc @ (get_vars e))
169 | end
170 | in
171 | get_pou_exprs elem
172 | |> List.fold_left ~init:[] ~f:(fun acc expr -> acc @ (get_vars expr))
173 |
174 | let filter_exprs ~f elem =
175 | let rec aux acc stmt =
176 | let get_nested stmts =
177 | List.fold_left
178 | stmts
179 | ~init:[]
180 | ~f:(fun acc s -> acc @ (aux [] s))
181 | in
182 | let rec get_nested_exprs acc = function
183 | | S.ExprBin (_,e1,_,e2) -> begin
184 | acc @
185 | [e1] @ (get_nested_exprs acc e1) @
186 | [e2] @ (get_nested_exprs acc e2)
187 | end
188 | | S.ExprUn (_,_,e) -> begin
189 | acc @ [e] @ (get_nested_exprs acc e)
190 | end
191 | | S.ExprVariable _ | S.ExprConstant _ | S.ExprFuncCall _ -> acc
192 | in
193 | let apply_filter (exprs : S.expr list) =
194 | List.filter exprs ~f
195 | in
196 | match stmt with
197 | | S.StmExpr (_, e) -> begin
198 | [e] @ (get_nested_exprs [] e)
199 | |> apply_filter
200 | |> List.append acc
201 | end
202 | | S.StmElsif (_, cond_s, ss) -> begin
203 | (get_nested [cond_s]) @
204 | (get_nested ss)
205 | |> apply_filter
206 | |> List.append acc
207 | end
208 | | S.StmIf (_, cond_s, body_ss, elsif_ss, else_ss) -> begin
209 | (get_nested [cond_s]) @
210 | (get_nested body_ss) @
211 | (get_nested elsif_ss) @
212 | (get_nested else_ss)
213 | |> apply_filter
214 | |> List.append acc
215 | end
216 | | S.StmCase (_, cond_s, case_sels, else_ss) ->
217 | begin
218 | let case_stmts =
219 | List.fold_left
220 | case_sels
221 | ~init:[]
222 | ~f:(fun acc case_sel -> begin
223 | acc @
224 | (get_nested case_sel.case) @
225 | (get_nested case_sel.body)
226 | end)
227 | in
228 | (get_nested [cond_s]) @
229 | (case_stmts) @
230 | (get_nested else_ss)
231 | |> apply_filter
232 | |> List.append acc
233 | end
234 | | S.StmFor (_, ctrl, body_stmts) -> begin
235 | (get_nested [ctrl.assign]) @
236 | [ctrl.range_end; ctrl.range_step] @
237 | (get_nested body_stmts)
238 | |> apply_filter
239 | |> List.append acc
240 | end
241 | | S.StmWhile (_, cond_stmt, ss) -> begin
242 | (get_nested [cond_stmt]) @
243 | (get_nested ss)
244 | |> apply_filter
245 | |> List.append acc
246 | end
247 | | S.StmRepeat (_, body_stmts, cond_stmt) -> begin
248 | (get_nested body_stmts) @
249 | (get_nested [cond_stmt])
250 | |> apply_filter
251 | |> List.append acc
252 | end
253 | | S.StmFuncCall (_, _, func_params) -> begin
254 | let func_params_stmts = List.fold_left
255 | func_params
256 | ~init:[]
257 | ~f:(fun acc fp -> acc @ [fp.stmt])
258 | in
259 | (get_nested func_params_stmts)
260 | |> apply_filter
261 | |> List.append acc
262 | end
263 | | S.StmExit _ | S.StmContinue _ | S.StmReturn _ -> acc
264 | in
265 | let all_stmts = get_pou_stmts elem in
266 | List.fold_left
267 | all_stmts
268 | ~init:[]
269 | ~f:(fun acc stmt -> acc @ (aux [] stmt))
270 |
271 | let get_ti_by_name_exn elem var_name =
272 | let (tis : TI.t list) = List.fold_left
273 | (get_var_decls elem)
274 | ~init:[]
275 | ~f:(fun acc vardecl -> begin
276 | if String.equal (S.VarDecl.get_var_name vardecl) var_name then
277 | acc @ [(S.VarDecl.get_var_ti vardecl)]
278 | else
279 | acc
280 | end)
281 | in
282 | List.nth_exn tis 0
283 |
284 | (** Bound declaration of global variables in global env. *)
285 | let fill_global_env env = function
286 | | S.IECFunction _ | S.IECFunctionBlock _ | S.IECProgram _ | S.IECType _ | S.IECClass _ | S.IECInterface _ -> env
287 | | S.IECConfiguration (_, cfg) ->
288 | List.fold_left cfg.variables ~f:(fun s v -> Env.add_vdecl s v) ~init:env
289 |
290 | let create_envs elems =
291 | (** Bound declaration of local variables to given env. *)
292 | let fill_pou_env (elem : S.iec_library_element) env =
293 | let r = List.fold_left (S.get_pou_vars_decl elem)
294 | ~init:env
295 | ~f:(fun s v -> Env.add_vdecl s v)
296 | in [r]
297 | in
298 | let global_env = Env.mk_global () in
299 | let global_env =
300 | List.fold_left elems ~f:(fun gs e -> fill_global_env gs e) ~init:global_env
301 | in
302 | List.fold_left elems
303 | ~f:(fun envs e ->
304 | Env.mk global_env (S.get_pou_id e)
305 | |> fill_pou_env e
306 | |> List.append envs)
307 | ~init:[global_env]
308 |
309 | let eval_array_capacity subranges =
310 | List.fold_left
311 | subranges
312 | ~init:(0)
313 | ~f:(fun acc (sr : S.arr_subrange) -> begin
314 | let mul = if phys_equal acc 0 then 1 else acc in
315 | (mul * (sr.arr_upper - sr.arr_lower + 1))
316 | end)
317 |
--------------------------------------------------------------------------------
/src/bin/iec_checker.ml:
--------------------------------------------------------------------------------
1 | open Core
2 | open IECCheckerCore
3 | open IECCheckerParser
4 | open IECCheckerLib
5 | open IECCheckerAnalysis
6 | module S = Syntax
7 | module Lib = CheckerLib
8 | module TI = Tok_info
9 | module W = Warn
10 | module WO = Warn_output
11 |
12 | (** Format of files given to the checker *)
13 | type input_format_ty =
14 | | InputST (** Structured Text source code *)
15 | | InputXML (** PLCOpen schemes *)
16 | | InputSELXML (** Schweitzer Engineering Laboratories XML Format *)
17 |
18 | type parse_results = S.iec_library_element list * Warn.t list
19 |
20 | (** The path of the temporary file created when the `-m` option is set. *)
21 | let merged_file_path = "merged-input.st"
22 |
23 | let parse_with_error (lexbuf: Lexing.lexbuf) : parse_results =
24 | let tokinfo lexbuf = TI.create lexbuf in
25 | let l = Lexer.initial tokinfo in
26 | try (Parser.main l lexbuf), [] with
27 | | Lexer.LexingError msg ->
28 | [], [(W.mk_from_lexbuf lexbuf "LexingError" msg)]
29 | | Parser.Error ->
30 | [], [(W.mk_from_lexbuf lexbuf "ParserError" "")]
31 | | e ->
32 | [], [(W.mk_from_lexbuf lexbuf "UnknownError" (Exn.to_string e))]
33 |
34 | let parse_stdin () : parse_results option =
35 | match In_channel.input_line In_channel.stdin with
36 | | None -> None
37 | | Some code -> begin
38 | let lexbuf = Lexing.from_string code in
39 | lexbuf.lex_curr_p <- { lexbuf.lex_curr_p with pos_fname = "stdin" };
40 | Some(parse_with_error lexbuf)
41 | end
42 |
43 | let parse_st_file (filename : string) : parse_results =
44 | let inx = In_channel.create filename in
45 | let lexbuf = Lexing.from_channel inx in
46 | lexbuf.lex_curr_p <- { lexbuf.lex_curr_p with pos_fname = filename };
47 | let (elements, warns) = parse_with_error lexbuf in
48 | In_channel.close inx;
49 | (elements, warns)
50 |
51 | let parse_xml_file (filename : string) : parse_results =
52 | let inx = In_channel.create filename in
53 | let program = Plcopen.reconstruct_from_channel inx in
54 | In_channel.close inx;
55 | let lexbuf = Lexing.from_string program in
56 | lexbuf.lex_curr_p <- { lexbuf.lex_curr_p with pos_fname = filename };
57 | let (elements, warns) = parse_with_error lexbuf in
58 | (elements, warns)
59 |
60 | (** [parse_sel_xml_file] Parse an SEL XML file located on [filepath]. If the
61 | file contains the valid XML, it returns parse results, otherwise None. *)
62 | let parse_sel_xml_file (filepath : string) : parse_results option =
63 | let inx = In_channel.create filepath in
64 | let program_opt = Sel.reconstruct_from_channel_opt inx in
65 | In_channel.close inx;
66 | match program_opt with
67 | | Some(program) -> begin
68 | let lexbuf = Lexing.from_string program in
69 | lexbuf.lex_curr_p <- { lexbuf.lex_curr_p with pos_fname = filepath };
70 | Some(parse_with_error lexbuf)
71 | end
72 | | None -> None
73 |
74 | let endswith s1 s2 =
75 | let len1 = String.length s1 and len2 = String.length s2 in
76 | if len1 < len2 then false
77 | else
78 | let sub = String.sub s1 ~pos:(len1 - len2) ~len:(len2) in
79 | String.equal sub s2
80 |
81 | (** [walkthrough_directory] Recursively traverse [path] and return absolute
82 | paths to the files with the given [suffix]. *)
83 | let walkthrough_directory path suffix =
84 | let rec aux result = function
85 | | f::_ when (Caml.Sys.file_exists f &&
86 | Caml.Sys.is_directory f) -> begin
87 | Caml.Sys.readdir f
88 | |> Array.to_list
89 | |> List.map ~f:(Filename.concat f)
90 | |> List.fold_left
91 | ~init:[]
92 | ~f:(fun acc p -> begin
93 | if Caml.Sys.is_directory p then
94 | acc @ (aux result [p])
95 | else if endswith p suffix then
96 | acc @ [p]
97 | else
98 | acc
99 | end)
100 | |> aux result
101 | end
102 | | f::fs -> aux (f::result) fs
103 | | [] -> result
104 | in
105 | aux [] [path]
106 |
107 | (** [get_files_to_check] Return a list of the files to be checked. *)
108 | let get_files_to_check paths in_fmt =
109 | let suffix = match in_fmt with
110 | | InputST -> ".st"
111 | | InputXML | InputSELXML -> ".xml"
112 | in
113 | List.fold_left
114 | paths
115 | ~init:[]
116 | ~f:(fun acc p -> acc @ walkthrough_directory p suffix)
117 |
118 | (** Collects paths to files that should be parsed.
119 | If there are directories among [paths], this functions recursively
120 | traverses them and collects nested files there. *)
121 | let collect_paths paths in_fmt =
122 | if List.exists paths ~f:(fun p -> String.equal p "-") then
123 | ["-"]
124 | else
125 | get_files_to_check paths in_fmt
126 |
127 | (** [start_repl] Start the iec-checker REPL. *)
128 | let start_repl interactive =
129 | if interactive then Printf.printf "> ";
130 | Out_channel.flush stdout;
131 | parse_stdin ()
132 |
133 | (** [parse_file] Parse file with the given path. *)
134 | let parse_file path in_fmt verbose : parse_results option =
135 | if verbose then Printf.printf "Parsing %s ...\n" path;
136 | match in_fmt with
137 | | InputST -> Some(parse_st_file path)
138 | | InputXML -> Some(parse_xml_file path)
139 | | InputSELXML -> begin
140 | parse_sel_xml_file path
141 | end
142 |
143 | module ReturnCode = struct
144 | let ok = 0
145 | let fail = 1
146 | let not_found = 127
147 | end
148 |
149 | let remove_file path =
150 | if Caml.Sys.file_exists path then
151 | Caml.Sys.remove path
152 |
153 | let cleanup out_path = remove_file out_path
154 |
155 | (** Merges contents of files [paths] creating a temporary file [out_path]. *)
156 | let merge_files paths out_path =
157 | remove_file out_path;
158 | let oc = Out_channel.create ~append:true ~fail_if_exists:true ~perm:0o755 out_path in
159 | List.iter paths ~f:(fun path ->
160 | Out_channel.output_string oc (In_channel.read_all path));
161 | Out_channel.close_no_err oc
162 |
163 | (** [run_checker] Run program on the file with [path] and returns the
164 | error code. *)
165 | let run_checker path in_fmt out_fmt create_dumps merged verbose (interactive : bool) : int =
166 | let (read_stdin : bool) = (String.equal "-" path) || (String.is_empty path) in
167 | if (not read_stdin && not (Caml.Sys.file_exists path)) then
168 | let err =
169 | W.mk_internal ~id:"FileNotFoundError"
170 | (Printf.sprintf "File %s doesn't exists" path)
171 | in
172 | WO.print_report [err] out_fmt;
173 | ReturnCode.not_found
174 | else
175 | let results_opt =
176 | if read_stdin then start_repl interactive
177 | else parse_file path in_fmt verbose
178 | in
179 | match results_opt with
180 | | None -> ReturnCode.ok
181 | | Some(elements, parser_warns) -> begin
182 | let envs = Ast_util.create_envs elements in
183 | let cfgs = Cfg.create_cfgs elements in
184 | if create_dumps then (
185 | let src_file = (if read_stdin then "stdin"
186 | else if merged then Filename.basename path
187 | else path)
188 | in
189 | let dst_file = Printf.sprintf "%s.dump.json" src_file in
190 | Dump.create_dump ~dst_file elements envs cfgs);
191 | let decl_warns = Declaration_analysis.run elements envs in
192 | let unused_warns = Unused_variable.run elements in
193 | let ud_warns = Use_define.run elements in
194 | let lib_warns = Lib.run_all_checks elements envs cfgs (not verbose) in
195 | WO.print_report (
196 | parser_warns @
197 | decl_warns @
198 | unused_warns @
199 | ud_warns @
200 | lib_warns)
201 | out_fmt;
202 | if List.is_empty parser_warns then ReturnCode.ok else ReturnCode.fail
203 | end
204 |
205 | let create_file path =
206 | Out_channel.create ~perm:0o755 path |> Out_channel.close_no_err
207 |
208 | let () =
209 | Clap.description "Static analysis of IEC 61131-3 programs ";
210 |
211 | let in_str =
212 | Clap.default_string
213 | ~short: 'i'
214 | ~long: "input-format"
215 | ~description:
216 | "Format of the input files. Supported formats:
217 | + st - Structured Text source
218 | + xml - PLCOpen XML
219 | + selxml - Schweitzer Engineering Laboratories XML"
220 | ~placeholder: "INPUT_FORMAT"
221 | "st"
222 | in
223 | let input_format = match in_str with
224 | | s when String.equal "st" s -> InputST
225 | | s when String.equal "xml" s -> InputXML
226 | | s when String.equal "selxml" s -> InputSELXML
227 | | s -> begin
228 | Printf.eprintf "Unknown input format '%s'.\n" s;
229 | Printf.eprintf "Available formats: 'st', 'xml' and 'selxml'\n";
230 | exit ReturnCode.fail
231 | end
232 | in
233 |
234 | let of_str =
235 | Clap.default_string
236 | ~short: 'o'
237 | ~long: "output-format"
238 | ~description:
239 | "Output format for the checker messages. Supported formats: 'plain' and 'json'."
240 | ~placeholder: "OUTPUT_FORMAT"
241 | "plain"
242 | in
243 | let output_format = match of_str with
244 | | s when String.equal "plain" s -> WO.Plain
245 | | s when String.equal "json" s -> WO.Json
246 | | s -> begin
247 | Printf.eprintf "Unknown output format '%s'. Supported: 'plain' and 'json'.\n" s;
248 | exit ReturnCode.fail
249 | end
250 | in
251 |
252 | let d =
253 | Clap.flag
254 | ~set_short: 'd'
255 | ~set_long: "dump"
256 | ~description:
257 | (Printf.sprintf
258 | "Create dump files of the processed files in the JSON format. \
259 | These files will contain the structure of the processed source files and \
260 | can be used from plugins and external tools. \
261 | Note: The dump file path will always be `%s` if the `-m` option is enabled."
262 | merged_file_path)
263 | false
264 | in
265 |
266 | let m =
267 | Clap.flag
268 | ~set_short: 'm'
269 | ~set_long: "merge"
270 | ~description:
271 | "Merge input files in the single file before running the checker. \
272 | This option is useful if the project is split into several files that \
273 | represent the same program. \
274 | Note: name collisions in the input files are forbidden."
275 | false
276 | in
277 |
278 | let v =
279 | Clap.flag
280 | ~set_short: 'v'
281 | ~set_long: "verbose"
282 | ~unset_short: 'q'
283 | ~unset_long: "quiet"
284 | ~description: "Show additional messages from the checker."
285 | false
286 | in
287 |
288 | let i =
289 | Clap.flag
290 | ~set_short: 'I'
291 | ~set_long: "interactive"
292 | ~unset_long: "non-interactive"
293 | ~description: "Accept input from stdin."
294 | false
295 | in
296 |
297 | let paths =
298 | Clap.list_string
299 | ~description:
300 | "Paths to source files or directories to check."
301 | ~placeholder: "PATHS"
302 | ()
303 | in
304 |
305 | Clap.close ();
306 |
307 | if List.is_empty paths then begin
308 | Printf.eprintf "No input files!\n\n";
309 | Clap.help ();
310 | exit ReturnCode.fail
311 | end
312 |
313 | else
314 | let paths' = collect_paths paths input_format in
315 | (* Disable the merge option if there is only one input file. *)
316 | let m = if m && phys_equal 1 (List.length paths') then false else m in
317 | let success =
318 | if m then (
319 | (* Merge all the input files to the single file and analyze it. *)
320 | remove_file merged_file_path;
321 | create_file merged_file_path;
322 | merge_files paths merged_file_path;
323 | let rc = run_checker merged_file_path input_format output_format d m v i in
324 | remove_file merged_file_path;
325 | phys_equal rc ReturnCode.ok)
326 | else (
327 | (* Run the checker for each file and collect all the warnings. *)
328 | List.fold_left paths'
329 | ~f:(fun return_codes f -> return_codes @ [run_checker f input_format output_format d m v i])
330 | ~init:[]
331 | |> List.for_all ~f:(phys_equal ReturnCode.ok))
332 | in
333 | if success
334 | then exit ReturnCode.ok else exit ReturnCode.fail
335 |
--------------------------------------------------------------------------------