FormalTalk-23: ERP /1 Compiler
Декларативна мова
з обмеженими імперативними властивостями
для програмування «МІА: Документообіг» 1.0
Передмова
Колись в мене була мрія зробити ERP систему зі своєю мовою програмування, це означало би, що ERP система достатньо формалізована і стабільна. Ця мова бачилася мені максимально обмеженою і простою, на рівні конфігураційних скриптів. Головним чином мова створена для реконфігурації маршрутів документів по папках, для чого потрібна рекомпіляція ерланг модулів. Також мова анонсує поки що єдиний імперативний оператор, що дозволяє перепаковувати документи та їх поля. В майбутньому — це повноцінна мова для модулів бізнес-процесів BPE, схем даних KVS, та візуальних форм.
м.Вишневе, 2023
Анотація
Основна ідея та мотивація — створити DSL навколо бібліотек ERP/1 [KVS,BPE,NITRO,FORM] з повним контролем над оператором call, а також нерекурсивним імреративним оператором присвоювання assign. Інші синтаксичні конструкції — декларативні, що відображені в операторах route, result, document, notify. Окремі частини (преамбули) синтаксичних конструкцій form і event можуть містити імперативну прелюдію для підготування змінних, яка складається з послідовності імперативних операторів.
Мова підтримує маніпуляцію списками, деконструкцію структур, паттерн-мачінг, систему аксесорів до структур, система типів — мінімально-необхідна для компіляції в BEAM байт-код та унаслідована від системи типів Erlang AST. У мові відсутні синитаксичні конструкції циклів, умовних операторів та неконтрольованої рекурсії. Однак, неконтрольовану рекурсію можна створити в BPMN дефініціях (як корекурсивний процес), тому верифікатор моделей BPE який перевіряє FormalTalk BPE файл містить детектор циклів в call-графах.
У статті представлена конфігурація системи «МІА: Документообіг» на анонсованій мові програмування «FormalTalk» або ft. Розкажемо тут тільки як компілювати патерн-мачінг правила для функцій routeTo та routeFrom, які використовуються для визначення папок користувачів, які отримають посилання на поточний документ на певній стадії обробки.
Нотація Бекуса-Наура
Мова підтримує три типи файлів: KVS, FORM, BPE. Кожен модуль може містити наступні синтаксичні конструкції: import (нефункіональна); record, event, route, form, notice (функціональні). Кожна синтаксична функціональна конструкція містить свій обмежений спосіб компіляції патерн-мачінга для певного класу функцій, що використовуються в МІА:Документообіг.
mod := 'module' name spec clauses
spec := 'kvs' | 'bpe' | 'form'
name := word | binary | string
args := [] | name args
clause := 'import' name
| 'record' name args dec
| 'notice' name args dec
| 'event' name args dec
| 'route' name args dec
| 'form' name args dec
dec := 'begin' decls 'end'
clauses := clause | clause clauses
decls := decl | decl '|' decls
decl := word '=' args ':' union
| word '=' args
| args
| 'document' word word '[' buttons ']' '[' fields ']'
| 'result' '[' conts ']' args
union := args | args '+' union
conts := args | args '|' conts
fields := args | args '|' fields
buttons := args | args '|' buttons
Слова мови
Будь-яка послідовність слів мови розділених пробілом трактується як імʼя функції і її аргументи:
args := [] | word args
Якщо args використовується в декларативних конструкціях, то всі слова означаються параметри, якщо ж args використовуються в імперативних конструкціях decls, то перший аргумент означає імʼя функції, як в call, assign.
Оскільки мотивація ft — це мінімізація неконтрольованої рекурсії, всі викливи функцій повинні бути контрольованими або бібліотечними, а сам компілятор повинен знаходити та ідентифікувати цикли в call-графах. Тому компілятор ft розуміє тільки вручну додані схеми компіляції для певного словника вбудованих чи розширюваних функцій.
Слова, які входять до складу args можуть бути типізованими або нетипізованими. Якщо в середині слова зустрічається символ :, то таке слово типізоване.
terminal := binary | list | atom | int
word := terminal—':'—terminal | terminal
Друге слово terminal після : визначає наступні скорочення:
"\"...\"" — binary
"'...'" — list
":..." — atom
"..." — var
Приклади:
123:integer
commit:atom == :commit
'commit' == commit:list
commit == commit:var
Аксесори структур
Аксесор крапка . є синтаксичною конструкцією доступу до іменованих структур полів. Якщо слово мови містить аксесор . — це означає, що слово зліва повинно містити типову специфікацію, яка містить словник всіх полів, з яким буде звірятися слово справа.
Аксесор тільда ~ є синтаксичною конструкцією доступу до іменованих структур обʼєктів, які не містять типової інформації, такі як map, dict.
У випадку коли аксесор тільда ~ застосовується до елементів типів-структур — застосовується схема компіляції :kvs.getfield, :kvs.setfield. У випадку коли аксесор крапка ~ застосовується до елементів типів-структур — застосовується схема компіляції безпосередньої маніпуляції зі структурами {, }, описаними в наступному параграфі.
Списки та структури
Якщо слово містить на свому початку та кінці символи [ і ], воно визначає синтаксичну конструкцію формування списків list, dict та map. Елементи списків — окремі слова — перелічуються через кому ,. Вони можуть бути: змінними, словами з аксесорами, типізованими змінними та контстантами. Колои елементи списку містять рівність = вони формують структуру dict. Коли елементи списку містять -> вони формують структуру map. Приклади:
[x] — cons(x,[])
[a=1,b=2] — dict([{a,1},{b,2}])
[a=>1,b=>2] — map([{a,1},{b,2}])
[x.id,y.cat] — cons(access(id,x),
cons(access(cat,y),[])
Якщо слово мови містить в кінці символ }, а також символ { в середині або на початку, то таке слово визначає конструктор стркутури або її паттерн-мачінг (деконструктор). Якщо символ { зустрічається в середині слова, то символи від початку до нього визначають тип структури-конструктора або змінну цього типу, як показано в прикладах:
{x,y} — tuple([x,y])
ERP.Person{x=a,y=b} — record("ERP.Person",[{x,a},{y,b}])
a{x=1} — record(inferType("a"),[{x,1}])
Конфігурація «МІА: Документообіг»
Тут показана конфігурація «Вхідного Документа» та всіх необхідних файлів для компіляції цієї частини CRM.ERP.UNO.
KVS модулі
KVS модулі призначені для визначення метанінформації бізнес-обʼєктів, детального опису типів їх полів.
module routeProc kvs
record routeProc
begin id = [] : list
| operation = [] : [] + atom
| feed = [] : [] + binary
| type = [] : atom
| folder = [] : binary
| users = [] : binary + list
| folderType = "personal" : binary
| callback = [] : [] + function
| reject = false : false + boolean
| options = [] : [] + list end
Щоб побачити внутрішнє представлення цього модуля, наберіть в Elixir консолі наступну команду:
> :ft.console ['parse','file','priv/kvs/routeProc.kvs' ]
{:ok,
{:module, "routeProc", :kvs,
[
{:record, {:name, "routeProc"}, [],
[
{:field, "id", ["[]"], {:type, ["list"]}},
{:field, "operation", ["[]"], {:type, ["[]", "atom"]}},
{:field, "feed", ["[]"], {:type, ["[]", "binary"]}},
{:field, "type", ["[]"], {:type, ["atom"]}},
{:field, "folder", ["[]"], {:type, ["binary"]}},
{:field, "users", ["[]"], {:type, ["binary", "list"]}},
{:field, "folderType", ["personal"], {:type, ["binary"]}},
{:field, "callback", ["[]"], {:type, ["[]", "function"]}},
{:field, "reject", ["false"], {:type, ["false", "boolean"]}},
{:field, "options", ["[]"], {:type, ["[]", "list"]}}
]}
]}}
FORM модулі
FORM модулі призначені для визначення структури форм які репрезентують певні бізнес-обʼєкти, що повинні бути визначені до цього в модулях KVS.
module inputForm form
fun id begin ERP.inputOrder end
form new name doc:ERP.inputOrder options
begin pid = options.pid
| user = options.user
| regBind = orgPath user
| pid = options.pid
| corrOpt = options
| corrOpt.postback = postback
| corrOpt.required = true
| coorOpt = dict
| coorOpt.users = doc.coordination
| document "inputOrder" name
[ butOk title { postback :inputOrder doc.id pid }
| butCancel "Скасувати" { :cancel postback :inputOrder doc.id pid }
| butTemplate "Шаблон" { :templates :create :inputOrder } on postback=:create ]
[ project comboLookup "Відхилено з коментарем"
| urgent bool "Терміново" required
| id string 'Номер документа'
| seq_id string "Унікальний номер"
| to comboLookupVec "Первинний розгляд" CRM.Forms.Person regBind
| nomenclature comboLookup "Номенклатура" CRM.Forms.DeedCat '/crm/deeds'
| document_type string "Вид документа"
| signed string "Підписав"
| generic corr controlTask corrOpt
| date calendar "Дата документа"
| dueDate calendar "Термін виконання" min=:erlang.date()
| note textarea "Примітка"
| add_sheets number 'К-ть аркушів додатків'
| bizTask_initiator comboLookupVec CRM.Forms.Person "/acc"
| modified_by comboLookupVec CRM.Forms.Person "/acc"
| registered_by comboLookup "Реєстратор" required CRM.Forms.Person regBind
| generic topic controlTask corrOpt
| generic coordination doc.coordination ] end
Щоб побачити внутрішнє представлення цього модуля, наберіть в Elixir консолі наступну команду:
> :ft.console ['parse','file','priv/form/input.form' ]
{:ok,
{:module, "inputForm", :form,
[
{:event, {:name, "id"}, [], [call: ["ERP.inputOrder"]]},
{:form, {:name, "new"}, {:args, ["name", "doc:ERP.inputOrder", "options"]},
[
{:assign, "pid", {:args, ["options.pid"]}},
{:assign, "user", {:args, ["options.user"]}},
{:assign, "regBind", {:args, ["orgPath", "user"]}},
{:assign, "pid", {:args, ["options.pid"]}},
{:assign, "corrOpt", {:args, ["options"]}},
{:assign, "corrOpt.postback", {:args, ["postback"]}},
{:assign, "corrOpt.required", {:args, ["true"]}},
{:assign, "coorOpt", {:args, ["dict"]}},
{:assign, "coorOpt.users", {:args, ["doc.coordination"]}},
{:document, "\"inputOrder\"", "name",
{:buttons,
[
{:button, "butOk", "title",
["{", "postback", ":inputOrder", "doc.id", "pid", "}"]},
{:button, "butCancel", "Скасувати",
["{", ":cancel", "postback", ":inputOrder", "doc.id", "pid", "}"]},
{:button, "butTemplate", "Шаблон",
["{", ":templates", ":create", ":inputOrder", "}", "on",
"postback=:create"]}
]},
{:fields,
[
{:field, "project", "comboLookup",
["Відхилено з коментарем"]},
{:field, "urgent", "bool", ["Терміново", "required"]},
{:field, "id", "string", ["Номер документа"]},
{:field, "seq_id", "string", ["Унікальний номер"]},
{:field, "to", "comboLookupVec",
["Первинний розгляд", "CRM.Forms.Person", "regBind"]},
{:field, "nomenclature", "comboLookup",
["Номенклатура", "CRM.Forms.DeedCat", "/crm/deeds"]},
{:field, "document_type", "string", ["Вид документа"]},
{:field, "signed", "string", ["Підписав"]},
{:field, "generic", "corr", ["controlTask", "corrOpt"]},
{:field, "date", "calendar", ["Дата документа"]},
{:field, "dueDate", "calendar",
["Термін виконання", "min=:erlang.date()",
"default=FormatDate.add_days(29)"]},
{:field, "note", "textarea", ["Примітка"]},
{:field, "add_sheets", "number",
["К-ть аркушів додатків"]},
{:field, "bizTask_initiator", "comboLookupVec",
["CRM.Forms.Person", "/acc"]},
{:field, "modified_by", "comboLookupVec",
["CRM.Forms.Person", "/acc"]},
{:field, "registered_by", "comboLookup",
["Реєстратор", ...]},
{:field, "generic", "topic", [...]},
{:field, "generic", "coordination", ...}
]}}
]}
]}}
BPE модулі
BPE модулі призначені для визначення логіки бізнес-процесів, опису маршрутів документів routeTo, routeFrom та notify.
module input bpe
import kvs/**/*
fun action broadcastEvent begin result [ ] proc stop end
fun action messageEvent payload={:next,*} begin output.bpe.action msg proc end
fun action messageEvent name=DocumentStatistics payload=payload
begin personal_stat proc.id executed payload | result [ ] proc stop end
fun action messageEvent name=RemoveDocumentStatistics payload=payload
begin remove_personal_stat proc.id executed payload | result [ ] proc stop end
fun action request to=Grouping
begin newDoc = proc.docs.hd | newProc = proc | newProc.docs = [newDoc]
| broadcast "Complete" | newProc = actionGen req proc newProc
| result [ general req newProc newDoc | stop ] newProc reply proc.executors end
fun action request from=gwRejected to=Implementation
begin newDoc = proc.docs.hd | newProc = proc | newProc.docs = [newDoc]
| broadcast "ImplementationRejected" | newProc = actionGen req proc newProc
| result [ general req newProc newDoc | stop ] newProc reply proc.executors end
fun action request to=Archive
begin newDoc = proc.docs.hd | newProc = proc | newProc.docs = [newDoc]
| unindexUrgent proc newDoc | newState = actionGen req proc newProc
| result [ general req newState newDoc | stop ] newState reply proc.executors end
route routeTo
begin (Cr,R):R,[] | (R,gwND):O,R | (gwND,Det):D,[] | (*,InC):A,To
| (gwC,I):A,To,toExecutors | (*,G):G,[];P,M | (*,A):A,[] end
route routeFrom
begin (R,gwND):R,[] | (Det,InC):D,[] | (InC,I):A,To,fromExecutors
| (I,gwC):A,To,fromExecutors | (gwR,I):G,[] | (*,G):A,To
| (*,A):G,[];O,R;U,RTo;R,[];D,[] end
Щоб побачити внутрішнє представлення цього модуля, наберіть в Elixir консолі наступну команду:
> :ft.console ['parse','file','priv/bpe/input.bpe' ]
{:ok,
{:module, "input", :bpe,
[
{:import, {:name, "kvs.*"}},
{:event, {:name, "action"}, {:args, ["broadcastEvent"]},
[{:result, [], {:args, ["proc", "stop"]}}]},
{:event, {:name, "action"},
{:args, ["messageEvent", "process", "userStarted=system:string"]},
[{:result, [], {:args, ["proc", "stop"]}}]},
{:event, {:name, "action"}, {:args, ["messageEvent", "name=TaskCreated"]},
[
{:assign, "newdoc", {:args, ["proc.docs.hd"]}},
{:assign, "doc.modified_by",
{:args, ["[doc.modified,msg.payload.notify]"]}},
{:assign, "proc.docs", {:args, ["[doc]"]}},
{:result, [], {:args, ["proc", "stop"]}}
]},
{:route, {:name, "routeTo"}, [],
[
call: ["(Cr,R):R,[]"],
call: ["(R,gwND):O,R"],
call: ["(gwND,Det):D,[]"],
call: ["(*,InC):A,To"],
call: ["(gwC,I):A,To,toExecutors"],
call: ["(*,G):G,[];P,M"],
call: ["(*,A):A,[]"]
]},
{:route, {:name, "routeFrom"}, [],
[
call: ["(R,gwND):R,[]"],
call: ["(Det,InC):D,[]"],
call: ["(InC,I):A,To,fromExecutors"],
call: ["(I,gwC):A,To,fromExecutors"],
call: ["(gwR,I):G,[]"],
call: ["(*,G):A,To"],
call: ["(*,A):G,[];O,R;U,RTo;R,[];D,[]"]
]}
]}}
В модулях BPE дозволяється використовувати скорочені назви задач, папок та полів:
$ cat stages.csv
CODE;TASK
I;Implementation
IC;ImplementationControl
gwC;gwConfirmation
gwS;gwSigned
gwA;gwAgreed
gwR;gwRejected
gwV;gwConverted
C;Certification
S;SignatureImposition
Se;Send
S2;Sending
D;Development
Ag;Agreement
A;Archive
gwND;gwNeedsDetermination
gwNC;gwNeedsCertification
Cr;Created
R;Registration
InC;InitialConsideration
Det;Determination
Cv;Convert
E;Execution
$ cat fields.csv
CODE;FIELD
R;registered_by
M;modified_by
BI;bizTask_initiator
I;initiator
E;executor
A;approvers
T;target
S;signatory
To;to
Tn;target_notify
$ cat folders.csv
CODE;FOLDER
G;grouping
P;processed
C;certification
O;out
I;created
U;urgently
S;signing
T;tasks
A;agreement
V;approval
D;determination
Se;sending
Rp;rejectedPersonal
R;resolutions
E;execution
LEEX/YECC консоль
Після генерації лексичних та синтаксичних парсерів
$ erlc lexer.xrl
$ erlc parser.yrl
зразу корисно написати функції доступу до згенерованих модулів:
-module(console).
-export([fst/1, snd/1, read/1, lex/1, parse/1, file/1, a/1, unicode/0, errcode/1]).
unicode() -> io:setopts(standard_io, [{encoding, unicode}]).
errcode({ok,_}) -> 0;
errcode({error,_}) -> 1.
fst({X,_}) -> {ok,X}.
snd({_,X}) -> {ok,X}.
file(F) -> lex(read(F)).
a(F) -> parse(file(F)).
read(F) ->
case file:read_file(F) of
{ok,B} -> {ok,unicode:characters_to_list(B)};
{error,E} -> {error,{file,F,E}} end.
lex({error,S}) -> {error,S};
lex({ok,S}) ->
case lexer:string(S) of
{ok,T,_} -> {ok,T};
{error,{L,A,X},_} -> {error,{lexer,L,A,element(2,X)}} end.
parse({error,T}) -> {error,T};
parse({ok,F}) ->
case parser:parse(F) of
{ok,AST} -> {ok,AST};
{error,{L,A,S}} -> {error,{parser,L,A,S}} end.
console([]) ->
io:format("~s ~s~n~n",
[ proplists:get_value(copyright,module_info(attributes)),
proplists:get_value(vsn,module_info(attributes))]),
io:format(" Usage := :ft.console [ args ] ~n"),
io:format(" args := command | command args ~n"),
io:format(" command := parse | lex | read | fst | snd | file ~n~n~n"),
io:format(" Sample: :ft.console ['parse','file','priv/form/input.form' ] ~n~n"),
0;
console(S) ->
lists:foldr(fun(I,Errors) -> R = lists:reverse(I),
lists:foldl(fun(X,A) ->
console:(list_to_atom(lists:concat([X])))(A) end,hd(R),tl(R)),
end, 0, string:tokens(S,[","])).
Компілятор
Так як Erlang та Elixir, FormalTalk використовує Erlang AST для паттерн-мачінг компіляції, яка охоплює всі 20 стадій компіляції мови Erlang. Фактично, це --- не компілятор (як і Elixir), а швидше звичайна AST транформація з мови більше вужчої та бідної (FormalTalk) в тюрінг-повну мову числення процесів (Erlang AST).
# Erlang AST definitions
def cons([]), do: {:nil,1}
def cons([x|t]), do: {:cons,1,x,cons(t)}
def mod(name), do: {:attribute,1,:module,name}
def compile_all(), do: {:attribute,1,:compile,:export_all}
def string(val), do: {:string,1,val}
def integer(val), do: {:integer,1,val}
def binary(val), do: {:bin,1,[{:bin_element,1,string(val),:default,[:utf8]}]}
def var(val), do: {:var,1,latom(val)}
def atom(val) when is_atom(val), do: {:atom,1,val}
def atom(val) when is_list(val), do: {:atom,1,latom(val)}
def fun(mod,name,arity), do: {:fun,1,{:function,mod,name,integer(arity)}}
# Imported module records
def record(recname,fields) do
recfields = :lists.map(fn {name,defx,type} ->
default = case defx do
[] -> cons([])
x when is_integer(x) -> integer(x)
x when is_list(x) -> string(x)
x when is_binary(x) -> binary(blist(x))
end
{:typed_record_field,{:record_field,1,atom(name),default},type}
end, fields)
{:attribute,1,:record,{recname,recfields}}
end
# routeProc{} onstructor invocation generation
def routeProcInvoke(folder,users,folderType,{mod,fun}) do
usrs = :lists.map fn x -> atom(x) end, users
ft = case folderType do
[] -> var('FT')
x when is_binary(x) -> binary(blist(x))
end
args = [{:folder,binary(folder)},
{:folderType,ft},
{:users,cons(usrs)},
{:callback,fun(atom(mod),atom(fun),2)}]
fields = :lists.map fn {name,val} -> {:record_field,1,atom(name),val} end, args
{:record,1,:routeProc,fields}
end
# Route clause generator
def routeClause(src,dst,commands) do
cmds = :lists.map fn {folder,users,folderType,callback} ->
routeProcInvoke(folder,users,folderType,callback) end, commands
{:clause,1,[{:tuple,1,[atom(:request),string(src),string(dst)]},
var('FT')],[],[cons(cmds)]}
end
# Route function generator
def routeFun(name,clauses) do
routeClauses = :lists.map fn {s,d,cmds} -> routeClause(s,d,cmds) end, clauses
{:function,1,name,2,routeClauses}
end
# Compile AST form to disk and reload
def compileForms(ast, out \\ 'priv/out/') do
:filelib.ensure_dir out
case :compile.forms ast, [:debug_info,:return] do
{:ok,name,beam,_} ->
:file.write_file out ++ alist(name) ++ '.beam', beam
:code.purge name
:code.load_file name
{name,beam}
_ ->
{[],[]}
end
end
Тестування компілятора
def testFile() do
[
mod(:inputProc),
compile_all(),
record(:routeProc,
[{:id,[],{:type,1,:list,[]}},
{:operation,[],{:type,1,:term,[]}},
{:feed,[],{:type,1,:term,[]}},
{:type,[],{:type,1,:term,[]}},
{:folder,[],{:type,1,:term,[]}},
{:users,[],{:type,1,:term,[]}},
{:folderType,[],{:type,1,:term,[]}},
{:callback,[],{:type,1,:term,[]}},
{:reject,[],{:type,1,:term,[]}},
{:options,[],{:type,1,:term,[]}}
]),
routeFun(:routeTo,
[{'gwConfirmation','Implementation',
[{'approval', ['to'], [], {'Elixir.CRM.KEP','toExecutors'}}
]}]),
]
end
Новостворений BEAM модуль в папці priv/out/ повинен містити скомпільовану функцію routeTo, яка повертає список обʼєктів ERP.routeProc.
def test() do
testFile() |> compileForms
[{:routeProc, [], [], [], [], "approval", [:to], [], _, [], []}]
= :inputProc.routeTo {:request, 'gwConfirmation', 'Implementation'}, []
:ok
end
Структура папки priv/erp.uno/:
├── bpe
│ ├── input.bpe
│ ├── internal.bpe
│ ├── org.bpe
│ ├── output.bpe
│ └── resolution.bpe
├── csv
│ ├── fields.csv
│ ├── folders.csv
│ ├── policies.csv
│ ├── rules.csv
│ └── stages.csv
├── form
│ └── input.form
├── kvs
│ ├── assistant.kvs
│ ├── assistantMark.kvs
│ ├── dict.kvs
│ ├── inputOrder.kvs
│ ├── location.kvs
│ ├── organization.kvs
│ ├── person.kvs
│ ├── project.kvs
│ └── routeProc.kvs
└── nitro
└── sample.nitro
Скомпільовані BEAM файли в папці priv/out/:
$ ls priv/out
bpe.input.beam bpe.resolution.beam kvs.dict.beam kvs.person.beam
bpe.internal.beam form.inputForm.beam kvs.inputOrder.beam kvs.project.beam
bpe.org.beam kvs.assistant.beam kvs.location.beam kvs.routeProc.beam
bpe.output.beam kvs.assistantMark.beam kvs.organization.beam nitro.sample.beam
Присвячую мову програмування ft Ліліані Левицькій та всім держслужбовцям України.
Бібліографія
Хоча для створення мови достатньо знання Erlang AST та його компіляції, а також бібліотек N2O.DEV, не можна сказати що на автора не вплинула наступна класична небагата бібліографія по декларативних мовах:
[1]. J.W.Lloyd. Practical Advantages of Declarative Programming. GULP-PRODE 1994.
[2]. E.Murru. Hands-on Low-code Application Development with Salesforce. 2020
[3]. P.Padawitz. Deduction and Declarative Programming. Cambridge. 1992.
[4]. H.Schwichtenberg,S.S.Wainer. Proofs and Computations. ASL, 2012.
[5]. Krishnamurti,Ramakrishnan. Practical Aspects of Decl. Languages. PAPL, 2002.
[6]. P.Roy, S.Haridi. Concepts, Techniques, and Models of Computer Programming. Swedish Institute of Compuer Science, 2003.
Матеріали
Для подальшого ознайомления з компілятором:
— Опис бібліотек KVS, BPE, NITRO, FORM: N2O.DEV
— Опис системи «МІА: Документообіг»: CRM.ERP.UNO
— Репозиторій проекту на Github: erpuno/ft
— Стаття про мову: FormalTalk: декларативна мова програмування з обмеженими імперативними властивостями. 2023.