Skip to content

Commit

Permalink
Add lexer for IEC 61131-3 Structured Text
Browse files Browse the repository at this point in the history
The new lexer uses sets of keywords to quickly categorize names and uses
regular expressions for other elements like numbers and punctuation.
  • Loading branch information
tali committed Mar 18, 2024
1 parent 0f4bb7d commit 99fe27d
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 2 deletions.
21 changes: 21 additions & 0 deletions lib/rouge/demos/iecst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// first-order lag derivative term
(*
Parameter K must be set to T / (T + T1)
*)
FUNCTION_BLOCK DT2

VAR_INPUT
IN : REAL;
K : REAL;
END_VAR
VAR_OUTPUT
Q : REAL;
END_VAR
VAR
H : REAL := 0.0;
END_VAR

BEGIN
Q := IN - H;
H := H + K * Q;
END_FUNCTION_BLOCK
8 changes: 7 additions & 1 deletion lib/rouge/guessers/disambiguation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,18 @@ def match?(filename)

Puppet
end

disambiguate '*.p' do
next Prolog if contains?(':-')
next Prolog if matches?(/\A\w+(\(\w+\,\s*\w+\))*\./)
next OpenEdge
end

disambiguate '*.st' do
next IecST if contains?('END_')
next IecST if contains?('(*')
next Smalltalk
end
end
end
end
92 changes: 92 additions & 0 deletions lib/rouge/lexers/iecst.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
module Lexers
class IecST < RegexLexer
tag 'iecst'
title "IEC 61131-3 Structured Text"
desc 'Structured text is a programming language for PLCs (programmable logic controllers).'
filenames '*.awl', '*.scl', '*.st'

mimetypes 'text/x-iecst'

def self.keywords
blocks = %w(
PROGRAM CONFIGURATION INITIAL_STEP INTERFACE FUNCTION_BLOCK FUNCTION ACTION TRANSITION
TYPE STRUCT STEP NAMESPACE LIBRARY CHANNEL FOLDER RESOURCE
VAR_ACCESS VAR_CONFIG VAR_EXTERNAL VAR_GLOBAL VAR_INPUT VAR_IN_OUT VAR_OUTPUT VAR_TEMP VAR
CONST METHOD PROPERTY
CASE FOR IF REPEAT WHILE
)
@keywords ||= Set.new %w(
AT BEGIN BY CONSTANT CONTINUE DO ELSE ELSIF EXIT EXTENDS FROM GET GOTO IMPLEMENTS JMP
NON_RETAIN OF PRIVATE PROTECTED PUBLIC RETAIN RETURN SET TASK THEN TO UNTIL USING WITH
__CATCH __ENDTRY __FINALLY __TRY
) + blocks + blocks.map {|kw| "END_" + kw}
end

def self.types
@types ||= Set.new %w(
ANY ARRAY BOOL BYTE POINTER STRING
DATE DATE_AND_TIME DT TIME TIME_OF_DAY TOD
INT DINT LINT SINT UINT UDINT ULINT USINT
WORD DWORD LWORD
REAL LREAL
)
end

def self.literals
@literals ||= Set.new %w(TRUE FALSE NULL)
end

def self.operators
@operators ||= Set.new %w(AND EQ EXPT GE GT LE LT MOD NE NOT OR XOR)
end

state :whitespace do
# Spaces
rule %r/\s+/m, Text
# // Comments
rule %r((//).*$\n?), Comment::Single
# (* Comments *)
rule %r(\(\*.*?\*\))m, Comment::Multiline
# { Comments }
rule %r(\{.*?\})m, Comment::Special
end

state :root do
mixin :whitespace

rule %r/'[^']+'/, Literal::String::Single
rule %r/"[^"]+"/, Literal::String::Symbol
rule %r/%[IQM][XBWDL][\d.]*|%[IQ][\d.]*/, Name::Variable::Magic
rule %r/\b(?:D|DT|T|TOD)#[\d_shmd:]*/i, Literal::Date
rule %r/\b(?:16#[\d_a-f]+|0x[\d_a-f]+)\b/i, Literal::Number::Hex
rule %r/\b2#[01_]+/, Literal::Number::Bin
rule %r/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i, Literal::Number::Float
rule %r/\b[\d.,_]+/, Literal::Number

rule %r/\b[A-Z_]+\b/i do |m|
name = m[0].upcase
if self.class.keywords.include?(name)
token Keyword
elsif self.class.types.include?(name)
token Keyword::Type
elsif self.class.literals.include?(name)
token Literal
elsif self.class.operators.include?(name)
token Operator
else
token Name
end
end

rule %r/S?R?:?=>?|&&?|\*\*?|<[=>]?|>=?|[-:^\/+#]/, Operator
rule %r/\b[a-z_]\w*(?=\s*\()/i, Name::Function
rule %r/\b[a-z_]\w*\b/i, Name
rule %r/[()\[\].,;]/, Punctuation
end
end
end
end
20 changes: 20 additions & 0 deletions spec/lexers/iecst_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

describe Rouge::Lexers::IecST do
let(:subject) { Rouge::Lexers::IecST.new }

describe 'guessing' do
include Support::Guessing

it 'guesses by filename' do
assert_guess :filename => 'foo.awl'
assert_guess :filename => 'foo.scl'
assert_guess :filename => 'foo.st', :source => "VAR ; END_VAR;"
end

it 'guesses by mimetype' do
assert_guess :mimetype => 'text/x-iecst'
end
end
end
2 changes: 1 addition & 1 deletion spec/lexers/smalltalk_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
include Support::Guessing

it 'guesses by filename' do
assert_guess :filename => 'foo.st'
assert_guess :filename => 'foo.st', :source => ''
end

it 'guesses by mimetype' do
Expand Down
53 changes: 53 additions & 0 deletions spec/visual/samples/iecst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// single line comment
(*
Block comment
*)
FUNCTION_BLOCK SAMPLE
TITLE='Sample function block'
VERSION : '1.0'
AUTHOR : 'Rouge'
FAMILY: 'sample'

CONST
Thousand : INT := 1_000;
TwoFiftyFive : BYTE := BYTE#255;
InHex : BYTE := 16#FF;
WithType : BYTE := BYTE#16#FF;
InBinary : BYTE := 2#1111_1111;

PI : REAL := 3.141592;
OneBillion : REAL := 1_000_000_000;
WithExponent : REAL := REAL#1e+9;
END_CONST

VAR_INPUT
a : REAL;
b : REAL;
limit : REAL;
END_VAR
VAR_OUTPUT
valid : BOOL;
END_VAR

VAR
last: REAL := 0.0;
END_VAR

VAR_TEMP
value: REAL;
END_VAR

BEGIN
IF a > 0 AND b > 0 THEN
value := sum / 2;
ELSIF a > 0 THEN
value := a;
ELSIF b > 0 THEN
value := b;
ELSE
value := last;
END_IF
last := value;

valid := value < limit;
END_FUNCTION_BLOCK

0 comments on commit 99fe27d

Please sign in to comment.