├── 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 | --------------------------------------------------------------------------------