Преглед изворни кода

Merge branch 'master' of https://github.com/gretmn102/QSP-LSP

gretmn102 пре 3 година
родитељ
комит
ea8cbcd855

+ 4 - 3
.gitignore

@@ -296,8 +296,6 @@ PublishScripts/
 *.nupkg
 # The packages folder can be ignored because of Package Restore
 **/packages/*
-# # except build/, which is used as an MSBuild target.
-# !**/packages/build/
 # Uncomment if necessary however generally it will be regenerated when needed
 #!**/packages/repositories.config
 # NuGet v3's project.json files produces more ignoreable files
@@ -394,4 +392,7 @@ paket-files/
 *.sln.iml
 
 /Output/
-/.ionide/
+/.ionide/
+/Utils/
+
+/Test/Mocks/

+ 8 - 0
.vscode/tasks.json

@@ -32,6 +32,14 @@
                 "TrimTrailingWhitespace"
             ],
             "problemMatcher": []
+        },
+        {
+            "label": "Watch",
+            "type": "shell",
+            "command": "build.cmd",
+            "args": [
+                "Watch"
+            ]
         }
     ]
 }

+ 12 - 0
Info/Ebnfqsp.txt

@@ -0,0 +1,12 @@
+ws = " " {" "}
+"a" = "a" ws
+'a' = 'a' {" "}
+Если нечто в кавычках - то после него обязательно следует как минимум один пробел, если в апострофах - 0 или больше пробелов
+escape symbol - "\"
+
+ident = ["*" | "$"] (underscore | letter) {letter | digit | underscore}
+
+assign = ["set" | "let"] ident ['[' expr ']'] ('=' ['+'|'-'] | ('+'|'-') '=') exprNotStartWithNeg
+call = ident exprNotStartWithNeg {"," expr}
+
+commentOp = "!" { ^("'"| nl | '"') | "'" { ^"'" | "''" } "'" | '"' { ^'"' | '""' } '"' }

+ 27 - 0
Info/ebnf.txt

@@ -0,0 +1,27 @@
+start = location {nl location};
+location = "#" anyExceptNl nl [statements nl] "-" anyExceptNl;
+statements = statement {nl statement | "&" stmtsOneLine};
+comment = "!" (stringConst | anyExceptNlOrStrStart);
+stmtsOneLine = expr {"&" expr} ["&" (assign | comment)];
+statement = assign | comment | expr | if | ifOneLine | act | actOneLine;
+actDef = "act" stringConst ["," stringConst] ":"
+actOneLine = actDef stmtsOneLine;
+act = actDef nl statements nl "end";
+ifDef = "if" expr ":"
+//ifOneLine = ifDef expr {"&" expr} ("&" ifOneLine | ["else" stmtsOneLine]);
+if = ifDef nl statements [nl "else" nl statements];
+varName = ["$"] letter {digit | letter}
+assign = ["set" | "let"] varName ["[" expr "]"] "=" expr;
+
+*PL ('The '&'door '&'is closed.')
+В строковые константы, в базовые описания локаций и названия базовых действий существует возможность вставлять значения выражений.
+
+Такие "подвыражения" должны находиться между двойных угловых скобок: "<<" и ">>", до и после которых может идти любой текст, включая подобные "подвыражения".
+
+Например, вместо оператора
+       pl 'i='+str(i)
+можно написать
+       pl 'i=<<i>>'
+PL STR(56)
+
+*p mid($s,i,1)

+ 15 - 0
Info/ebnf2.txt

@@ -0,0 +1,15 @@
+ifOne = ifBegin stmtsOne ["else" stmtsOne];
+ifBegin = "if" expr ":"
+actBegin = "act" expr ":"
+actOne = actBegin stmtsOne;
+stmtsOne = actOne | ifOne | assign { "&" assign } [actOne | ifOne]
+
+stmtsMultiNl = nl smtsMulti nl {stmtsMulti nl}
+
+actMulti = actBegin (stmtsOne | stmtsMultiNl "end");
+
+elif = ifBegin stmtsMultiNl ("end" | "else" (elif | stmtsMultiNl "end"));
+
+ifMulti = ifOne | elif = ifBegin (stmtsOne ["else" stmtsOne] | stmtsMultiNl ("end" | "else" (elif | stmtsMultiNl "end")))
+
+stmtsMulti = ifMulti | actMulti | assign { "&" assign } [ifMulti | actMulti]

+ 31 - 0
Info/expr.txt

@@ -0,0 +1,31 @@
+f1 = { '/'; '*' }
+f2 = { =, <, >, !, <>, <=, >=, =<, => }
+AND
+OR
+
+
+a * b + c -> (+ c (* a b))
+a + b * c -> (+ a (* b c))
+
+a f1 b f2 c -> (f2 c (f1 a b))
+a f2 b f1 c -> (f2 a (f1 b c))
+
+a AND b OR c -> (OR c (AND a b))
+a OR b AND c -> (OR a (AND b c))
+
+expr f2 expr
+
+
+* + = & |
+op { + = }
+a * b op c -> (op c (* a b))
+a op b * c -> (op a (* b c))
+
+a + b = c -> (= c (+ a b))
+a = b + c -> (= a (+ b c))
+op2 { & | }
+a = b op2 c -> (op2 c (= a b))
+a op2 b = c -> (op2 a (= b c))
+
+a & b | c -> (| c (& a b))
+a | b & c -> (| a (& b c))

+ 1 - 0
Info/keywords.txt

@@ -0,0 +1 @@
+ ACT ADDLIB ADDOBJ ADDQST AND ARRCOMP ARRPOS ARRSIZE $BACKIMAGE BCOLOR CLA CLEAR *CLEAR CLOSE CLR *CLR CLS CMDCLEAR CMDCLR COPYARR $COUNTER COUNTOBJ $CURACTS CURLOC DEBUG DELACT DELLIB DELOBJ DESC DISABLESCROLL DISABLESUBEX DYNAMIC DYNEVAL ELSE ELSEIF EXIT FCOLOR $FNAME FREELIB FSIZE FUNC GETOBJ GOSUB GOTO GS GT IF IIF INCLIB INPUT INSTR ISNUM ISPLAY JUMP KILLALL KILLOBJ KILLQST KILLVAR LCASE LCOLOR LEN LET LOC $MAINTXT MAX MENU MID MIN MOD MSECSCOUNT MSG NL *NL NO NOSAVE OBJ $ONACTSEL $ONGLOAD $ONGSAVE $ONNEWLOC $ONOBJADD $ONOBJDEL $ONOBJSEL OPENGAME OPENQST OR P *P PL *PL PLAY QSPVER RAND REFINT REPLACE RGB RND SAVEGAME SELACT SELOBJ SET SETTIMER SHOWACTS SHOWINPUT SHOWOBJS SHOWSTAT $STATTXT STR STRCOMP STRFIND STRPOS TRIM UCASE UNSEL UNSELECT USEHTML $USERCOM USER_TEXT USRTXT VAL VIEW WAIT XGOTO XGT



+ 30 - 0
Info/shortInfo.txt

@@ -0,0 +1,30 @@
+В названиях переменных / массивов допускаются любые символы, кроме ":,&,=,<,>,+,-,*,/,',",(,),!,[,]", запятых, пробелов и символов табуляции. Также не допускается в начале названия ставить цифры и использовать ключевые слова (названия функций / операторов) в качестве названий переменных. Не рекомендуется использовать длинные названия (более 10-15 символов).
+Все числовые переменные должны быть целочисленного типа.
+В названиях локаций, переменных, действий и предметов не важен регистр букв, т.е. "ДеньГи" и "деньги" - одна и та же локация
+Любой не оператор (если это не метка и не одна из форм "END") является выражением для вывода в основное окно описаний. Например, можно записать
+10) Для подвыражений, если нужно вывести последовательность "<<" на экран, используйте системную переменную "DISABLESUBEX". К примеру:
+
+       DISABLESUBEX=1
+       $text = '<<var>>'
+       'string <<var>>'
+       DISABLESUBEX=0
+
+
+
+11) Не обязательно записывать операторы / выражения в одной строке. Чтобы перейти на другую строку, нужно лишь в конце строки дописать " _" (пробел и символ подчёркивания). Строки
+       if a<5 and n-b>4+5+h/7*2 or t=4: p 'TTTTTTTTT' & cla & $f='Text Variable' & goto 'FFFF'
+       if a<5 and n-b> _
+               4+5+h/7*2 or  _
+               t=4: p 'TTTTTTTTT' _
+               & cla & $f='Text Variable' _
+               & goto 'FFFF'
+
+ЭКВИВАЛЕНТНЫ, т.е. воспринимаются движком одинаково.
+ 
+
+PS:
+1) Группа строк, разделённых " _", считается ОДНОЙ строкой (сообщения об ошибках также выводятся с учётом того, что это одна строка).
+
+2) После "OR" стоит не один, а ДВА пробела - первый пробел воспринимается движком как пробел, а второй - как часть " _". Это сделано для того, чтобы движок правильно обрабатывал операцию "OR" - не как "4+5+h/7*2 ort=4", а как "4+5+h/7*2 or t=4".
+ 

+ 6 - 0
QSParse/App.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
+    </startup>
+</configuration>

+ 172 - 0
QSParse/Ast.fs

@@ -0,0 +1,172 @@
+module Qsp.Ast
+open FsharpMyExtension
+
+type Op =
+    /// `+`
+    | Plus
+    /// `-`
+    | Minus
+    /// `*`
+    | Times
+    /// `/`
+    | Divide
+    /// `mod`
+    | Mod
+
+    /// `=`
+    | Eq
+    /// `>`
+    | Gt
+    /// `>=`
+    | Ge
+    /// `&lt;`
+    | Lt
+    /// `&lt;=`
+    | Le
+    /// `!` — то же самое, что и `&lt;>`
+    | Bang
+    /// `!` or `&lt;>`
+    | Ne
+    /// `=&lt;`
+    | El
+    /// `=>`
+    | Eg
+    /// `and`
+    | And
+    /// `or`
+    | Or
+type IsBinOpSymbolic = bool
+[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
+[<RequireQualifiedAccess>]
+module Op =
+    [<ReflectedDefinition>]
+    let toString = function
+        | Times -> "*"
+        | Divide -> "/"
+        | Mod -> "mod"
+        | Plus -> "+"
+        | Minus -> "-"
+        | Lt -> "<"
+        | Gt -> ">"
+        | Le -> "<="
+        | Eg -> "=>"
+        | Ge -> ">="
+        | El -> "=<"
+        | Ne -> "<>"
+        | And -> "and"
+        | Or -> "or"
+        | Eq -> "="
+        | Bang -> "!"
+
+    let ops =
+        Reflection.Reflection.unionEnum<Op>
+        |> Array.map (fun x ->
+            let IsBinOpSymbolic opName =
+                not <| String.exists FParsec.CharParsers.isLetter opName
+                : IsBinOpSymbolic
+            let y = toString x
+            x, (y, IsBinOpSymbolic y) )
+
+    let fromString =
+        let m = Array.map (fun (a, b) -> b, a) ops |> Map.ofArray
+        fun x -> match Map.tryFind x m with Some x -> x | None -> failwithf "not found %A" x
+type UnarOp =
+    /// `-`
+    | Neg
+    /// `obj`
+    | Obj
+    /// `no`
+    | No
+    /// `loc`
+    | Loc
+[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
+[<RequireQualifiedAccess>]
+module UnarOp =
+    [<ReflectedDefinition>]
+    let toString = function | Obj -> "obj" | Neg -> "-" | No -> "no" | Loc -> "loc"
+    let ops =
+        Reflection.Reflection.unionEnum<UnarOp>
+        |> Array.map (fun x -> x, toString x)
+    let fromString =
+        let m = Array.map (fun (a, b) -> b, a) ops |> Map.ofArray
+        fun x -> match Map.tryFind x m with Some x -> x | None -> failwithf "not found %A" x
+module Precedences =
+    type T = OpB of Op | PrefB of UnarOp
+    // &
+    // OR
+    // AND
+    // OBJ, NO
+    // =, <, >, !, <>, <=, >=, =<, =>
+    // +, -
+    // MOD
+    // *, /
+    // +, - (унарные)
+
+    let prec = function
+        | OpB Or -> 1
+        | OpB And -> 2
+        | PrefB No -> 3
+        | PrefB Loc | PrefB Obj -> 4 // `no obj 'apple'` equal `no (obj 'apple')`
+        // =     | <      | >      | !        | <>     | <=     | >=     | =>     | =<
+        | OpB Eq | OpB Lt | OpB Gt | OpB Bang | OpB Ne | OpB Le | OpB Ge | OpB Eg | OpB El-> 5
+        | OpB Plus | OpB Minus -> 6
+        | OpB Mod -> 7
+        | OpB Times | OpB Divide -> 8
+        | PrefB Neg -> 9
+
+type VarType =
+    /// `varName`, если к такой присвоить строковое значение, то интерпретатор попытается преобразовать ее в число. Если не получится, выбьет ошибку.
+    | ImplicitNumericType
+    /// `#varName`, если к такой присвоить строковое значение, то интерпретатор попытается преобразовать ее в число. Если не получится, выбьет ошибку.
+    | ExplicitNumericType
+    /// `$varName`, к такой переменной можно смело присваивать и число, и строку
+    | StringType
+type Var = VarType * string
+type StmtsOrRaw =
+    | Raw of string
+    | StaticStmts of Statement list
+and LineKind =
+    | StringKind of string
+    /// Это то, что заключено между `&lt;&lt; >>`
+    | ExprKind of Expr
+    /// `&lt;a href="exec: ...">some text&lt;/a>`
+    | HyperLinkKind of StmtsOrRaw * Line list
+/// Без переносов
+and Line = LineKind list
+
+and Value =
+    | Int of int
+    | String of Line list
+
+and Expr =
+    | Val of Value
+    | Var of var:Var
+    | Func of string * Expr list
+    | Arr of var:Var * Expr list
+    | UnarExpr of UnarOp * Expr
+    | Expr of Op * Expr * Expr
+
+and AssignWhat =
+    | AssignVar of var:Var
+    /// Ключом массива может быть значение любого типа
+    | AssignArr of var:Var * key:Expr
+    | AssignArrAppend of var:Var
+and Statement =
+    | Assign of AssignWhat * Expr
+    | AssignCode of AssignWhat * Statement list
+    | CallSt of string * Expr list
+    /// Вычисляется `expr` и посылается в `*pl`
+    | StarPl of Expr
+    | If of Expr * Statement list * Statement list
+    | Act of Expr list * Statement list
+    | For of var:Var * from:Expr * to':Expr * body:Statement list
+    | Label of string
+    | Comment of string
+    | Exit
+type LocationName = string
+/// ```qsp
+/// # location name
+/// 'asdf'
+/// - произвольный набор символов
+/// ```
+type Location = Location of LocationName * Statement list

+ 1216 - 0
QSParse/Defines.fs

@@ -0,0 +1,1216 @@
+module Qsp.Defines
+
+module Tools =
+    open FParsec
+    type 'a Parser = Parser<'a, unit>
+    let removeEmptyLines () =
+        Clipboard.getSet (fun str ->
+            // let x = System.Text.RegularExpressions.Regex.Replace(str, "^\n", "", System.Text.RegularExpressions.RegexOptions.Multiline)
+            // x
+            let ws = manySatisfy (fun c -> System.Char.IsWhiteSpace c && c <> '\n')
+            let wsLine = ws .>>? skipNewline
+            let p =
+                spaces
+                >>. many
+                    (
+                        ws >>. many1Satisfy ((<>) '\n')
+                        .>> (skipNewline <|> eof)
+                        .>> many wsLine
+                    )
+                |>> String.concat "\n"
+            match run p str with
+            | Success(x, _, _) -> x
+            | Failure(x, _, _) -> failwithf "%A" x
+        )
+    // removeEmptyLines()
+    module Show =
+        open FsharpMyExtension
+        open FsharpMyExtension.ShowList
+        let print tabsCount isFunction xs =
+            let tab = replicate 4 ' '
+            let showStr x =
+                showAutoParen "\""
+                    (showString
+                        (x
+                         |> String.collect (
+                             function
+                             | '"' -> "\\\""
+                             | '\\' -> "\\\\"
+                             | x -> string x )))
+            xs
+            |> List.collect (fun (descs, varName) ->
+                let desc =
+                    [
+                        yield showChar '['
+                        yield!
+                            descs
+                            |> List.map (fun x ->
+                                tab << showStr x)
+                        yield showChar ']' << showString " |> String.concat \"\\n\""
+                    ]
+                [
+                    yield showString "let dscr ="
+                    yield! List.map ((<<) tab) desc
+                    let signature =
+                        if isFunction then
+                            showString ", " << showString "failwith \"not implemented\""
+                        else
+                            empty
+                    yield
+                        showAutoParen "\"" (showString varName) << showString ", " << showString "dscr"
+                        << signature
+
+                ]
+            )
+            |> List.map ((<<) (showReplicate tabsCount tab))
+            |> joinEmpty "\n"
+            |> show
+
+    let parse tabsCount isFunction =
+        Clipboard.getSet (fun str ->
+            let description =
+                many1
+                    (pstring "///" >>. optional (skipChar ' ') >>. manySatisfy ((<>) '\n') .>> spaces)
+            let expr =
+                between
+                    (skipChar '"')
+                    (skipChar '"')
+                    (manySatisfy ((<>) '"'))
+            let p = spaces >>. many (description .>>. expr .>> spaces)
+            match run (p .>> eof) str with
+            | Success(xs, _, _) -> Show.print tabsCount isFunction xs
+            | Failure(x, _, _) -> failwithf "%A" x
+        )
+    // parse 2 true
+type VarType =
+    | Any
+    | String
+    | Numeric
+
+// type X () =
+//     member __.F(x:string, [<System.ParamArray>] args: string[]) =
+//         printfn "first"
+//         for arg in args do
+//             printfn "%A" arg
+//     member __.F(x:string, y:string, [<System.ParamArray>] args: string[]) =
+//         printfn "second"
+//         for arg in args do
+//             printfn "%A" arg
+// let x = X()
+// x.F("1")
+// x.F("1", y = "2")
+// x.F("1", y = "2", "3")
+
+type 'Func OverloadType =
+    | JustOverloads of (VarType [] * 'Func) list
+    /// Если говорить родным F#, то это:
+    /// ```fsharp
+    /// member __.F(x:Type1, y:Type2, [&lt;System.ParamArray>] args: Type3 []) =
+    /// ```
+    /// выражается так:
+    /// ```fsharp
+    /// {| Requireds: [ Type1; Type2 ]; ArgList: Type3 |}
+    /// ```
+    | ParamArrayOverload of {| Requireds: VarType []; ArgList: VarType |} * 'Func
+module Show =
+    open FsharpMyExtension.ShowList
+    let showVarType = function
+        | Any -> showString "any"
+        | String -> showString "string"
+        | Numeric -> showString "numeric"
+    let showParamArray typ =
+        // На Lua:
+        // ```lua
+        // function f(...)
+        //     -- ...
+        // end
+        // ```
+        // В Python:
+        // ```python
+        // def my_function(*argName):
+        //     # ...
+        // ```
+        // Но это то, как оно объявляется, а сигнатуру-то как написать?
+        // showChar '*' << showChar ':' << showVarType typ
+        showVarType typ << showSpace << showString "[]"
+    let showArgs varTypes =
+        varTypes
+        |> Array.map showVarType
+        |> List.ofArray
+        |> joins (showChar ',' << showSpace)
+    let printSignature (name:string) =
+        function
+        | JustOverloads xs ->
+            xs
+            |> List.map (fun (varTypes, _) ->
+                showString name << showParen true (showArgs varTypes)
+            )
+            |> lines
+        | ParamArrayOverload(x, _) ->
+            showString name
+            << showParen true
+                (showArgs x.Requireds << showChar ',' << showSpace
+                 << showParamArray x.ArgList)
+        >> show
+    let printFuncSignature (name:string) (returnType:VarType) =
+        function
+        | JustOverloads xs ->
+            xs
+            |> List.map (fun (varTypes, _) ->
+                showString name << showParen true (showArgs varTypes)
+                << showChar ':' << showSpace << showVarType returnType
+            )
+            |> lines
+        | ParamArrayOverload(x, _) ->
+            showString name
+            << showParen true
+                (showArgs x.Requireds << showChar ',' << showSpace
+                 << showParamArray x.ArgList)
+            << showChar ':' << showSpace << showVarType returnType
+        >> show
+
+let getFuncByOverloadType overloadType (inputArgs: _ []) =
+    let inputArgsLength = Array.length inputArgs
+    match overloadType with
+    | JustOverloads os ->
+        os
+        |> List.tryPick (fun (x, func) ->
+            if x.Length = inputArgsLength then
+                Some func
+            else None
+        )
+    | ParamArrayOverload(x, func) ->
+        if inputArgsLength < x.Requireds.Length then
+            None
+        else
+            Some func
+
+let arg x =
+    [[|x|] , ()] |> JustOverloads
+
+let unit' =
+    [[||] , ()] |> JustOverloads
+
+let args xs =
+    [xs |> Array.ofList, ()] |> JustOverloads
+
+let argsAndOptional xs opt =
+    [
+        xs |> Array.ofList, ()
+        xs @ [opt] |> Array.ofList, ()
+    ] |> JustOverloads
+
+let argList typ =
+    ({| Requireds = [||]; ArgList = typ |}, ())
+    |> ParamArrayOverload
+
+let argAndArgList x typ =
+    ({| Requireds = [|x|]; ArgList = typ |}, ())
+    |> ParamArrayOverload
+type VarName = string
+type Description = string
+
+let proceduresWithAsterix =
+    [
+        let dscr =
+            [
+                "`*CLEAR` или `*CLR` - очистка основного окна описаний."
+            ] |> String.concat "\n"
+        ("*clear":VarName), (dscr:Description), unit'
+        let dscr =
+            [
+                "`*CLEAR` или `*CLR` - очистка основного окна описаний."
+            ] |> String.concat "\n"
+        "*clr", dscr, unit'
+        let dscr =
+            [
+                "`*NL [выражение]` - переход на новую строку, затем вывод текста в основном окне описаний. Если `[выражение]` не указано, то перевод строки. Отличается от *PL порядком вывода текста."
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| String |], ()
+                [| |], ()
+            ] |> JustOverloads
+        "*nl", dscr, funcs
+        let dscr =
+            [
+                "`*P [выражение]` - вывод текста в основное окно описаний (по умолчанию находится слева сверху и не может быть отключено)."
+            ]|> String.concat "\n"
+        "*p", dscr, arg Any
+        let dscr =
+            [
+                "`*PL [выражение]` - вывод текста, затем переход на новую строку в основном окне описаний. Если `[выражение]` не указано, то перевод строки. Аналогичным образом можно вывести текст, просто написав нужное выражение вместо данного оператора. Например, строки:"
+                "```qsp"
+                "*PL $AAA+'989'"
+                "*PL 'Вы находитесь в парке'"
+                "*PL 'Преформатированная"
+                "строка'"
+                "```"
+                "и"
+                "```qsp"
+                "$AAA+'989'"
+                "'Вы находитесь в парке'"
+                "'Преформатированная"
+                "строка'"
+                "```"
+                "выполнят одно и то же действие."
+            ] |> String.concat "\n"
+        "*pl", dscr, arg Any
+    ]
+    |> List.map (fun (name, dscr, sign) -> name, (dscr, sign))
+    |> Map.ofList
+
+/// Заданные переменные
+let vars =
+    [
+        let dscr =
+            [
+                "содержит путь к файлу изображения локации. Изображение локации показывается в том случае, если значение данной переменной отлично от '' (не пустая строка) и файл изображения удалось загрузить."
+            ] |> String.concat "\n"
+        ("$backimage":VarName), (dscr:Description)
+        let dscr =
+            [
+                "Название выделенного действия."
+            ] |> String.concat "\n"
+        "$selact", dscr
+        let dscr =
+            [
+                "содержит название локации-счётчика. Локация-счётчик полезна для проверки выделенных предметов, введённого текста..."
+            ] |> String.concat "\n"
+        "$counter", dscr
+        let dscr =
+            [
+                "текущие действия в виде текста. Сохранив значение в переменной, восстановить действия можно в любой момент игры с помощью оператора `DYNAMIC`."
+            ] |> String.concat "\n"
+        "$curacts", dscr
+        let dscr =
+            [
+                "текст, находящийся в основном окне описаний. Также есть функция `maintxt`"
+            ] |> String.concat "\n"
+        "$maintxt", dscr
+        let dscr =
+            [
+                "содержит название используемого в данный момент шрифта. Если равна '' (пустая строка), то используется шрифт, заданный в настройках программы."
+            ] |> String.concat "\n"
+        "$fname", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика выбора действия. Данная локация полезна, к примеру, для вывода изображений или проигрывания звуков при выборе действий. Получить название выбранного действия можно через функцию `SELACT`."
+            ] |> String.concat "\n"
+        "$onactsel", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика загрузки состояния. Данная локация полезна для выполнения каких-либо действий после загрузки состояния игры."
+            ] |> String.concat "\n"
+        "$ongload", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика сохранения состояния. Данная локация полезна для выполнения каких-либо действий перед сохранением состояния игры."
+            ] |> String.concat "\n"
+        "$ongsave", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика перехода на новую локацию (аналог локации \"common\" в URQ). Может заменить часть функций локации-счётчика. Получить название локации, на которую был осуществлён переход, можно с помощью функции `CURLOC`."
+            ] |> String.concat "\n"
+        "$onnewloc", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика добавления предмета. При добавлении предмета локация вызывается с аргументом `$ARGS[0]` - названием добавленного предмета. Данная локация полезна, к примеру, для ограничения вместительности рюкзака."
+            ] |> String.concat "\n"
+        "$onobjadd", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика удаления предмета. При удалении предмета локация вызывается с аргументом `$ARGS[0]` - названием удалённого предмета. Данная локация полезна, к примеру, для проверки возможности удаления предмета."
+            ] |> String.concat "\n"
+        "$onobjdel", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика выбора предмета. Данная локация полезна, к примеру, для вывода меню предметов. Получить название выбранного предмета можно через функцию `SELOBJ`."
+            ] |> String.concat "\n"
+        "$onobjsel", dscr
+        let dscr =
+            [
+                "текст, находящийся в окне пользователя. Также есть функция `stattxt`"
+            ] |> String.concat "\n"
+        "$stattxt", dscr
+        let dscr =
+            [
+                "содержит название локации-обработчика строки ввода. Полезна при организации парсера (управление игрой с помощью строки ввода). Текущий текст строки ввода возвращает функция `USER_TEXT`."
+            ] |> String.concat "\n"
+        "$usercom", dscr
+        let dscr =
+            [
+                "название текущей локации, также можно использовать `curloc`"
+            ] |> String.concat "\n"
+        "$curloc", dscr
+        let dscr =
+            [
+                "содержит размер используемого в данный момент шрифта. Если равна 0, то используется размер, заданный в настройках программы. Относительно данного значения в HTML-режиме вычисляются размеры шрифтов тега \"FONT\"."
+            ] |> String.concat "\n"
+        "fsize", dscr
+        let dscr =
+            [
+                "содержит цвет текущего фона. Если равна 0, то используется цвет, заданный в настройках программы."
+            ] |> String.concat "\n"
+        "bcolor", dscr
+        let dscr =
+            [
+                "содержит цвет используемого в данный момент шрифта. Если равна 0, то используется цвет, заданный в настройках программы."
+            ] |> String.concat "\n"
+        "fcolor", dscr
+        let dscr =
+            [
+                "содержит текущий цвет ссылок. Если равна 0, то используется цвет, заданный в настройках программы."
+            ] |> String.concat "\n"
+        "lcolor", dscr
+        let dscr =
+            [
+                "если отлична от 0, включает возможность использования HTML в описании локации, в дополнительном описании, в списках действий и предметов, а также в диалоге ввода текста, вызываемого функцией `INPUT`. Выводимый текст распознаётся как HTML. Список поддерживаемых тегов и их атрибутов смотрите в приложении."
+            ] |> String.concat "\n"
+        "usehtml", dscr
+        let dscr =
+            [
+                "Массив, который содержит аргументы текущей локации. Подробнее расписано в процедуре `GOSUB` и ей подобной."
+            ] |> String.concat "\n"
+        "args", dscr
+        "#args", dscr
+        "$args", dscr
+        let dscr =
+            "Если текущая локация вызвана с помощью `FUNC` или создана динамически с помощью `DYNEVAL`, то по итогу вернется это значение. Ах да, эта переменная бывает нескольких типов: `result`, `#result` и `$result`. И возникает вопрос: а что, собственно, вернет, скажем, вызов `DYNEVAL(\"$result = 'string' & #result = 1\")`? Что ж, сработает только `$x = DYNEVAL(\"$result = 'string' & #result = 1\") & $x`, а `#x = DYNEVAL(\"$result = 'string' & #result = 1\") & #x` — выбьет ошибку про несоответствие данных, что довольно странно."
+        "result", dscr
+        "$result", dscr
+        "#result", dscr
+        let dscr =
+            [
+                "если значение переменной не равно 0, то отключается проверка идентификатора игры при загрузке состояния. Полезно для отладки."
+            ] |> String.concat "\n"
+        "debug", dscr
+        let dscr =
+            [
+                "если значение переменной не равно 0, то запрещает автопрокрутку текста при его выводе в основное или дополнительное окно описания локации."
+            ] |> String.concat "\n"
+        "disablescroll", dscr
+        let dscr =
+            [
+                "если значение переменной не равно 0, то запрещает использование \"подвыражений\" в тексте (например, значением `'<<5+6>>'` будет строка `'<<5+6>>'`, а не `'11'`)."
+            ] |> String.concat "\n"
+        "disablesubex", dscr
+        let dscr =
+            [
+                "Если её значение отлично от 0, то сохранение состояния игры пользователем невозможно."
+            ] |> String.concat "\n"
+        "nosave", dscr
+    ]
+    |> Map.ofList
+let functions =
+    let arg x (return':VarType) = arg x, return'
+    let unit' (return':VarType) = unit', return'
+    let args xs (return':VarType) = args xs, return'
+    let argList typ (return':VarType) = argList typ, return'
+    let argAndArgList x typ (return':VarType) = argAndArgList x typ, return'
+    [
+        let dscr =
+            [
+                "`RAND([#выражение 1],[#выражение 2])` - возвращает случайное число между числами `[#выражение 1]` и `[#выражение 2]`. Параметр `[#выражение 2]` может отсутствовать, при этом он принимается равным 0."
+            ] |> String.concat "\n"
+        let os =
+            [
+                [| Numeric; Numeric |], ()
+                [| Numeric |], ()
+            ] |> JustOverloads
+        "rand", dscr, (os, Numeric)
+        let dscr =
+            [
+                "возвращает название текущей локации, также можно использовать переменную `$curloc`"
+            ] |> String.concat "\n"
+        "curloc", dscr, unit' String
+        let dscr =
+            [
+                "возвращает случайное значение от 1 до 1000."
+            ] |> String.concat "\n"
+        "rnd", dscr, unit' Numeric
+        let dscr =
+            [
+                "`INPUT([$выражение])` - выводит окно ввода с приглашением [$выражение]. Возвращает введённый играющим текст, либо '' (пустая строка), если была нажата кнопка \"Отмена\"."
+            ] |> String.concat "\n"
+        "input", dscr, arg String String
+        let dscr =
+            [
+                "возвращает текст, находящийся в строке ввода. Синоним `usrtxt`"
+            ] |> String.concat "\n"
+        "user_text", dscr, unit' String
+        let dscr =
+            [
+                "возвращает текст, находящийся в строке ввода. Синоним `user_text`"
+            ] |> String.concat "\n"
+        "usrtxt", dscr, unit' String
+        let dscr =
+            [
+                "`MAX([выражение 1],[выражение 2], ...)` - возвращает максимальное из значений выражений-аргументов. Если передан один аргумент, то считается, что указано имя массива - в этом случае поиск максимального элемента происходит среди строковых (если название массива указано со знаком \"$\") или среди числовых значений элементов массива. Например:"
+                "```qsp"
+                "MAX(1,2,5,2,0) & ! вернёт 5"
+                "MAX(a,b,c) & ! вернёт максимальное из значений переменных"
+                "MAX('aa','ab','zz') & ! вернёт 'zz'"
+                "MAX('a') & ! вернёт максимальное из числовых значений элементов массива \"A\""
+                "MAX('$b') & ! вернёт максимальное из строковых значений элементов массива \"B\""
+                "```"
+            ] |> String.concat "\n"
+        "max", dscr, argList Any Any
+        let dscr =
+            [
+                "`MIN([выражение 1],[выражение 2], ...)` - возвращает минимальное из значений выражений-аргументов. Если передан один аргумент, то считается, что указано имя массива - в этом случае поиск минимального элемента происходит среди строковых (если название массива указано со знаком \"$\") или среди числовых значений элементов массива."
+            ] |> String.concat "\n"
+        "min", dscr, argList Any Any
+        let dscr =
+            [
+                "`IIF([#выражение],[выражение_да],[выражение_нет])` - возвращает значение выражения [выражение_да], если [#выражение] верно, иначе значение выражения [выражение_нет]."
+            ] |> String.concat "\n"
+        "iif", dscr, args [Numeric; Any; Any] Any
+        let dscr =
+            [
+                "`RGB([#выражение 1],[#выражение 2],[#выражение 3])` - возвращает код цвета на основе 3-х числовых аргументов. [#выражение 1], [#выражение 2] и [#выражение 3] определяют соответственно уровни красного, зелёного и синего цветов. Все значения аргументов должны быть в отрезке [0, 255]. Данная функция используется совместно с системными переменными `BCOLOR`, `FCOLOR` и `LCOLOR`."
+            ] |> String.concat "\n"
+        "rgb", dscr, args [Numeric; Numeric; Numeric] Numeric
+        let dscr =
+            [
+                "`ISPLAY([$выражение])` - проверяет, проигрывается ли файл с заданным названием в текущий момент времени и возвращает -1, если файл воспроизводится, иначе 0."
+            ] |> String.concat "\n"
+        "isplay", dscr, arg String Numeric
+        let dscr =
+            [
+                "возвращает количество миллисекунд, прошедших с момента начала игры."
+            ] |> String.concat "\n"
+        "msecscount", dscr, unit' Numeric
+        let dscr =
+            [
+                "`DESC([$выражение])` - возвращает текст базового описания локации с заданным в [$выражение] названием."
+            ] |> String.concat "\n"
+        "desc", dscr, arg String String
+        let dscr =
+            [
+                "возвращает текст, находящийся в основном окне описаний. Также есть переменная `$maintxt`"
+            ] |> String.concat "\n"
+        "maintxt", dscr, unit' String
+        let dscr =
+            [
+                "возвращает текст, находящийся в окне пользователя. Также есть переменная `$stattxt`"
+            ] |> String.concat "\n"
+        "stattxt", dscr, unit' String
+        let dscr =
+            [
+                "возвращает версию интерпретатора в формате \"X.Y.Z\""
+            ] |> String.concat "\n"
+        "qspver", dscr, unit' String
+        let dscr =
+            [
+                "`FUNC([$выражение],[параметр 1],[параметр 2], ...)` - обработка локации с названием `[$выражение]`. Указанные параметры передаются в массиве `ARGS`. Результат функции равен значению `$RESULT` при возврате строкового значения или `RESULT` при возврате числового значения. Если при обработке локации были установлены и `RESULT`, и `$RESULT`, то предпочтение отдаётся строковому значению. После обработки локации предыдущие значения `ARGS` и `RESULT` восстанавливаются. Примеры:"
+                "```qsp"
+                "PL 4 + FUNC('функция') & ! обработка локации \"функция\" как функции. Массив ARGS пуст. Результат передается через `$RESULT` или `RESULT`, в зависимости от кода обрабатываемой локации."
+                "PL FUNC($name, 1) * 78 & ! обработка локации с названием в $name как функции. `ARGS[0]` равен 1."
+                "MSG \"text\" + FUNC($name, \"строка\", 2) & ! обработка локации с названием в $name как функции. `$ARGS[0]` содержит строку \"строка\", `ARGS[1]` равен 2."
+                "```"
+            ] |> String.concat "\n"
+        "func", dscr, argList Any Any
+        let dscr =
+            [
+                "То же, что и `FUNC`, только возвращает строчный тип:"
+                "```qsp"
+                "# start"
+                "$func('toString', 1) & ! -> '1'"
+                "-"
+                ""
+                "# toString"
+                "$result = ARGS[0]"
+                "-"
+                "```"
+            ] |> String.concat "\n"
+        "$func", dscr, argList Any String
+        let dscr =
+            [
+                "`DYNEVAL([$выражение],[параметр 1],[параметр 2], ...)` - возвращает значение указанного выражения. Функция позволяет вычислять значения динамически сгенерированных выражений. Указанные параметры передаются в массиве `ARGS`, а после вычисления выражения предыдущие значения `ARGS` восстанавливаются. Примеры:"
+                "```qsp"
+                "DYNEVAL('3+4')"
+                "PL DYNEVAL('mid(\"abcd\",2,1)+\"qwerty\"')"
+                "PL DYNEVAL($test + ' + val(\"<<$test>>\")')"
+                "DYNEVAL(\" $args[0] <> 'текст' \", 'строка')"
+                "$x = DYNEVAL(\"$result = 'текст'\") & $x"
+                "```"
+            ] |> String.concat "\n"
+        "dyneval", dscr, argAndArgList String Any Any
+        let dscr =
+            [
+                "возвращает количество предметов в рюкзаке."
+            ] |> String.concat "\n"
+        "countobj", dscr, unit' Numeric
+        let dscr =
+            [
+                "возвращает название выделенного предмета."
+            ] |> String.concat "\n"
+        "selobj", dscr, unit' String
+        let dscr =
+            [
+                "`GETOBJ([#выражение])` - возвращает название предмета в рюкзаке, расположенного в заданной позиции. Индексация предметов рюкзака ведётся с 1."
+                "Если предмета с заданным индексом не существует, возвращается пустая строка ('')."
+                ""
+                "Примеры:"
+                "```qsp"
+                "GETOBJ(1) & ! вернёт название первого предмета в рюкзаке"
+                "GETOBJ(COUNTOBJ) &! вернёт название последнего добавленного предмета"
+                "```"
+                ""
+                "Код, подсчитывающий в массиве `OBJECTS` число предметов с одинаковым названием:"
+                "```qsp"
+                "i = 1"
+                ":loop"
+                "IF i <= COUNTOBJ:"
+                "    OBJECTS[$GETOBJ(i)] = OBJECTS[$GETOBJ(i)] + 1"
+                "    i = i + 1"
+                "    JUMP 'loop'"
+                "END"
+                "```"
+            ] |> String.concat "\n"
+        "getobj", dscr, arg Numeric String
+        let dscr =
+            [
+                "`ARRCOMP([#выражение 1],[$выражение 2],[$выражение 3])` - возвращает индекс элемента массива с названием `[$выражение 2]`, соответствующего регулярному выражению `[$выражение 3]`. Поиск начинается с элемента номер `[#выражение 1]`; индексация элементов массива ведётся с нуля. Параметр `[#выражение 1]` может отсутствовать, при этом он принимается равным 0. Если элемент не найден, функция возвращает -1."
+                "Поиск происходит среди текстовых элементов массива. Примеры:"
+                "```qsp"
+                "ARRCOMP(0,'A','This') & ! найдёт строку 'This' среди текстовых элементов массива \"A\" (или вернёт -1, если такого значения не существует)"
+                "ARRCOMP(2,'A','abc\\d+') & ! найдёт строку, соответствующую регулярному выражению \"abc\\d+\", в текстовых значениях массива \"A\" (первые два элемента массива игнорируются)"
+                "ARRCOMP(0,'A','.*string.*') & ! аналогично предыдущему примеру, но поиск осуществляется по всем текстовым элементам массива"
+                "ARRCOMP('A','This') & ! эквивалентно 1-му варианту"
+                "```"
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| Numeric; String; String |], ()
+                [| String; String |], ()
+            ] |> JustOverloads
+        "arrcomp", dscr, (funcs, String)
+        let dscr =
+            [
+                "`STRCOMP([$выражение],[$шаблон])` - проводит сравнение строки `[$выражение]` на соответствие регулярному выражению `[$шаблон]`. Возвращает -1, если строка соответствует шаблону, иначе 0. Сравни с функцией `STRFIND`."
+            ] |> String.concat "\n"
+        "strcomp", dscr, args [String; String] Numeric
+        let dscr =
+            [
+                "`STRFIND([$выражение],[$шаблон],[#номер])` - возвращает подстроку в строке `[$выражение]`, соответствующую группе с номером `[#номер]` регулярного выражения `[$шаблон]`. Если подстрока с указанным номером отсутствует, то возвращается пустая строка. Нумерация групп подстрок начинается с 1. Если параметр `[#номер]` отсутствует или равен 0, то возвращается подстрока, соответствующая всему регулярному выражению `[$шаблон]`. Примеры:"
+                "```qsp"
+                "STRFIND(' идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 0) & ! -> ''"
+                "STRFIND('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 1) & ! -> 'идти'"
+                "STRFIND('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 2) & ! -> 'к'"
+                "STRFIND('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 3) & ! -> 'пещере'"
+                "STRFIND('идти к пещере', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 4) & ! -> 'пещере'"
+                "STRFIND('искать ключ', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 1) & ! -> 'искать'"
+                "STRFIND('искать', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 0) & ! -> ''"
+                "STRFIND('искать', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 1) & ! -> ''"
+                "STRFIND('искать', '^(\\S+)(\\s(\\S+)(\\s(\\S+))?)?$', 1) & ! -> 'искать'"
+                "STRFIND('идти к дому', 'к\\s(\\S+)', 0) & ! -> 'к дому'"
+                "STRFIND('идти к дому', 'к\\s(\\S+)') & ! -> 'к дому'"
+                "STRFIND('идти к дому', 'к\\s(\\S+)', 1) & ! -> 'дому'"
+                "STRFIND('идти к своему дому', 'к\\s(\\S+)', 1) & ! -> 'своему'"
+                "```"
+            ] |> String.concat "\n"
+        "strfind", dscr, (argsAndOptional [ String; String ] Numeric, String)
+        let dscr =
+            [
+                "`STRPOS([$выражение],[$шаблон],[#номер])` - возвращает позицию символа, с которого начинается вхождение подстроки в строке `[$выражение]`, соответствующей группе с номером `[#номер]` регулярного выражения `[$шаблон]`. Если подстрока с указанным номером отсутствует, то возвращается 0. Нумерация групп подстрок начинается с 1. Если параметр `[#номер]` отсутствует или равен 0, то возвращается позиция символа, с которого начинается вхождение подстроки, соответствующей всему регулярному выражению `[$шаблон]`."
+                "Примеры:"
+                "```qsp"
+                "STRPOS(' идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 0) & ! -> 0"
+                "STRPOS('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 1) & ! -> 1"
+                "STRPOS('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 2) & ! -> 6"
+                "STRPOS('идти к пещере', '^(\\S+)\\s(\\S+)\\s(\\S+)$', 3) & ! -> 8"
+                "STRPOS('идти к пещере', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 4) & ! -> 8"
+                "STRPOS('искать ключ', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 1) & ! -> 1"
+                "STRPOS('искать', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 0) & ! -> 0"
+                "STRPOS('искать', '^(\\S+)\\s(\\S+)(\\s(\\S+))?$', 1) & ! -> 0"
+                "STRPOS('искать', '^(\\S+)(\\s(\\S+)(\\s(\\S+))?)?$', 1) & ! -> 1"
+                "STRPOS('идти к дому', 'к\\s(\\S+)', 0) & ! -> 6"
+                "STRPOS('идти к дому', 'к\\s(\\S+)') & ! -> 6"
+                "STRPOS('идти к дому', 'к\\s(\\S+)', 1) & ! -> 8"
+                "STRPOS('идти к своему дому', 'к\\s(\\S+)', 1) & ! -> 8"
+                "```"
+            ] |> String.concat "\n"
+        "strpos", dscr, (argsAndOptional [ String; String ] Numeric, Numeric)
+        let dscr =
+            [
+                "возвращает текущие действия в виде текста. Сохранив значение в переменной, восстановить действия можно в любой момент игры с помощью оператора `DYNAMIC`."
+            ] |> String.concat "\n"
+        "curacts", dscr, unit' String
+        let dscr =
+            [
+                "`ARRPOS([#выражение 1],[$выражение 2],[выражение 3])` - возвращает индекс элемента массива с названием `[$выражение 2]`, равного значению выражения `[выражение 3]`. Поиск начинается с элемента номер `[#выражение 1]`; индексация элементов массива ведётся с нуля. Параметр [#выражение 1] может отсутствовать, при этом он принимается равным 0. Если указанное значение не найдено, функция возвращает -1."
+                "Чтобы найти числовое значение в массиве, нужно в `[$выражение 2]` подставить лишь название массива (в кавычках), для строкового - название массива с символом \"$\" перед его названием. Примеры:"
+                "`ARRPOS(0,'$A','This')` - найдёт строку 'This' в текстовых значениях массива \"A\" (или вернёт -1, если такого значения не существует)"
+                "`ARRPOS(2,'A',65)` - найдёт число 65 в числовых значениях массива \"A\" (при этом первые два элемента массива игнорируются)"
+                "`ARRPOS('$B','test')` - поиск строки 'test' среди текстовых значений массива \"B\""
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| Numeric; String; Any |], ()
+                [| String; Any |], ()
+            ] |> JustOverloads
+        "arrpos", dscr, (funcs, Numeric)
+        let dscr =
+            [
+                "`ARRSIZE([$выражение])` - возвращает число элементов в массиве с названием `[$выражение]`."
+            ] |> String.concat "\n"
+        "arrsize", dscr, arg String Numeric
+        let dscr =
+            [
+                "`INSTR([#выражение 1],[$выражение 2],[$выражение 3])` - возвращает номер позиции символа, с которого начинается вхождение строки `[$выражение 3]` в строку `[$выражение 2]` (или 0, если такой строки нет). Поиск начинается с символа номер `[#выражение 1]`. Параметр `[#выражение 1]` может отсутствовать, при этом он принимается равным 1. Примеры:"
+                "`INSTR(1,'ABCDefgh','BC')` равно `2`"
+                "`INSTR(1,'ABCDefgh','Be')` равно `0`"
+                "`INSTR('abcdef','abc')` равно `1`"
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| Numeric; String; String |], ()
+                [| String; String |], ()
+            ] |> JustOverloads
+        "instr", dscr, (funcs, Numeric)
+        let dscr =
+            [
+                "`ISNUM([$выражение])` - функция проверяет, все ли символы в строке являются цифрами (учитывая знак \"-\" в начале, прилегающие пробелы и символы табуляции). Если в указанной строке есть хотя бы один символ - не-цифра (исключая возможный \"-\" в начале и прилегающие пробелы / символы табуляции), то функция возвращает 0 (ложь), иначе -1 (истина)."
+                "Функция полезна при проверке введённой играющим строки на число. Примеры:"
+                "`ISNUM('9999 ')` равно `-1`"
+                "`ISNUM(' -888')` равно `-1`"
+                "`ISNUM('777a6')` равно `0`"
+            ] |> String.concat "\n"
+        "isnum", dscr, arg String Numeric
+        let dscr =
+            [
+                "`LCASE([$выражение])` - возвращает строку маленьких букв, полученную изменением регистра букв исходной строки `[$выражение]`. Пример:"
+                "```qsp"
+                "LCASE('TExT#') & ! 'text#'"
+                "```"
+            ] |> String.concat "\n"
+        "lcase", dscr, arg String String
+        let dscr =
+            [
+                "`LEN([$выражение])` - возвращает длину строки `[$выражение]`."
+            ] |> String.concat "\n"
+        "len", dscr, arg String Numeric
+        let dscr =
+            [
+                "`MID([$выражение],[#выражение 1],[#выражение 2])` - вырезает из строки `[$выражение]` строку, которая начинается с символа номер `[#выражение 1]` и имеет длину `[#выражение 2]`. Индексация символов в строке ведётся с 1."
+                "Параметр `[#выражение 2]` может отсутствовать, при этом вырезается вся строка, начиная с символа [#выражение 1]. Примеры:"
+                "```qsp"
+                "MID('abcd', 1, 2) равно 'ab'"
+                "MID('abcd', 2, 3) равно 'bcd'"
+                "MID('abcd', 2) равно 'bcd'"
+                "```"
+            ] |> String.concat "\n"
+        "mid", dscr, (argsAndOptional [ String; Numeric ] Numeric, String)
+        let dscr =
+            [
+                "`REPLACE([$выражение 1],[$выражение 2],[$выражение 3])` - заменяет в строке [$выражение 1] все вхождения строки [$выражение 2] строкой [$выражение 3]. Если [$выражение 3] отсутствует или указана пустая строка, то удаляет в исходной строке все вхождения искомой строки. Примеры:"
+                "`REPLACE('test', '12', '4')` равно `'test'`"
+                "`REPLACE('test', 'e', 's')` равно `'tsst'`"
+                "`REPLACE('test', 't', '34')` равно `'34es34'`"
+                "`REPLACE('test', 't')` равно `'es'`"
+            ] |> String.concat "\n"
+        "replace", dscr, (argsAndOptional [ String; String ] String, String)
+        let dscr =
+            "Синоним `REPLACE`"
+        "$replace", dscr, (argsAndOptional [ String; String ] String, String)
+        let dscr =
+            [
+                "`STR([#выражение])` - переводит число (числовое выражение) `[#выражение]` в соответствующую строку. Например,"
+                "`PL STR(56)` выведет строку `56`."
+            ] |> String.concat "\n"
+        "str", dscr, arg Numeric String
+        let dscr =
+            [
+                "`TRIM([$выражение])` - удаляет прилегающие пробелы и символы табуляции из `[$выражение]`. Затем возвращает полученную строку. Пример:"
+                "`TRIM('     TRIM TEST        ')` равно `'TRIM TEST'`"
+            ] |> String.concat "\n"
+        "trim", dscr, arg String String
+        let dscr =
+            [
+                "`UCASE([$выражение])` - возвращает строку больших букв, полученную изменением регистра букв исходной строки `[$выражение]`. Пример:"
+                "```qsp"
+                "UCASE('TexT#') & ! -> 'TEXT#'"
+                "```"
+            ] |> String.concat "\n"
+        "ucase", dscr, arg String String
+        let dscr =
+            [
+                "`VAL([$выражение])` - переводит строку цифр `[$выражение]` в соответствующее число. Если [$выражение] равно '' (пустая строка) или если оно содержит не-цифры, то возвращается 0."
+            ] |> String.concat "\n"
+        "val", dscr, arg String Numeric
+        let dscr =
+            [
+                "возвращает название выделенного действия."
+            ] |> String.concat "\n"
+        "selact", dscr, unit' String
+    ]
+    |> List.map (fun (name, dscr, sign) -> name, (dscr, sign))
+    |> Map.ofList
+/// Да-да, всё это — процедуры: они что-то выполняют и никуда не перемещают.
+let procedures =
+    [
+        let dscr =
+            [
+                "`OPENGAME [$выражение]` - если `[$выражение]` равно '' (пустая строка) или отсутствует, то вызов окна загрузки состояния игры, иначе загрузка состояния из указанного файла. См. также локацию-обработчик события загрузки игры."
+            ] |> String.concat "\n"
+        let os =
+            [
+                [||], ()
+                [| String |], ()
+            ] |> JustOverloads
+        "opengame", dscr, os
+        let dscr =
+            [
+                "`CMDCLEAR` или `CMDCLR` - очистка строки ввода."
+            ] |> String.concat "\n"
+        "cmdclear", dscr, unit'
+        let dscr =
+            [
+                "`CMDCLEAR` или `CMDCLR` - очистка строки ввода."
+            ] |> String.concat "\n"
+        "cmdclr", dscr, unit'
+        let dscr =
+            [
+                "`SAVEGAME [$выражение]` - если `[$выражение]` равно '' (пустая строка) или отсутствует, то вызов окна сохранения состояния игры, иначе сохранение состояния в указанный файл. См. также локацию-обработчик события сохранения игры."
+            ] |> String.concat "\n"
+        "savegame", dscr, arg String
+        let dscr =
+            [
+                "`OPENQST [$выражение]` - открытие и запуск заданного файла игры. При использовании данного оператора, не происходит удаления переменных, удаления предметов инвентаря, очистки дополнительного описания и строки ввода, а также остановки проигрываемых файлов (для этого в начале загружаемой игры можно выполнить \"KILLALL & CLS & CLOSE ALL\")."
+            ] |> String.concat "\n"
+        "openqst", dscr, arg String
+        let dscr =
+            [
+                "`ADDQST [$выражение]` - из заданного файла игры добавляет все локации, названия которых отсутствуют среди текущих игровых локаций. Загруженные локации полностью эквивалентны локациям из основного файла игры."
+            ] |> String.concat "\n"
+        "addqst", dscr, arg String
+        let dscr =
+            "Синоним `addqst`"
+        "addlib", dscr, arg String
+        let dscr =
+            "Синоним `addqst`"
+        "inclib", dscr, arg String
+        let dscr =
+            [
+                "KILLQST - удаляет все локации, добавленные с помощью оператора `ADDQST`"
+            ] |> String.concat "\n"
+        "killqst", dscr, unit'
+        "freelib", dscr, unit'
+        "dellib", dscr, unit'
+        let dscr =
+            [
+                "`DELACT [$название]` или `DEL ACT [$название]` - удаляет действие из списка действий на локации (если такое действие существует). Например:"
+                "```qsp"
+                "DELACT 'Идти вперед'"
+                "DELACT $SELACT"
+                "```"
+            ] |> String.concat "\n"
+        "delact", dscr, arg String
+        let dscr =
+            [
+                "CLA - очистка списка текущих действий."
+            ] |> String.concat "\n"
+        "cla", dscr, unit'
+        let dscr =
+            [
+                "`ADDOBJ [$название],[$путь к файлу изображения]` или `ADD OBJ [$название],[$путь к файлу изображения]` - добавление предмета с заданным изображением в рюкзак."
+                "К предметам добавляется новый с названием `[$название]` и изображением `[$путь к файлу изображения]`."
+                ""
+                "Параметр `[$путь к файлу изображения]` может отсутствовать, при этом предмет добавится без изображения."
+                ""
+                "Обратите внимание - для использования одинаковых предметов инвентаря, например денег, патронов и т.п., лучше использовать дополнительную переменную, обозначающую количество этих предметов, чтобы не загромождать инвентарь списком из 137 предметов Рубль / Патрон. Для хранения числа предметов можно использовать массивы, индексируемые через строки:"
+                ""
+                "```qsp"
+                "OBJECTS['деньги'] = 12"
+                "OBJECTS['патроны'] = 137"
+                "'Количество: <<OBJECTS[$getobj(countobj)]>>'"
+                "```"
+            ] |> String.concat "\n"
+        "addobj", dscr, argsAndOptional [String] String
+        let dscr =
+            [
+                "`DELOBJ [$название]` или `DEL OBJ [$название]` - удаление предмета из рюкзака, если таковой имеется. Также см. локацию-обработчик удаления предмета."
+            ] |> String.concat "\n"
+        "delobj", dscr, arg String
+        let dscr =
+            [
+                "`KILLOBJ [#выражение]` - удаление предмета, расположенного в заданной позиции. Если параметр [#выражение] не указан, то очистка рюкзака."
+                "Индексация предметов рюкзака ведётся с 1. Также см. локацию-обработчик удаления предмета."
+            ] |> String.concat "\n"
+        let os =
+            [
+                [||], ()
+                [| Numeric |], ()
+            ] |> JustOverloads
+        "killobj", dscr, os
+        let dscr =
+            [
+                "`UNSELECT` или `UNSEL` - отмена выбора предмета. При выборе играющим какого-либо предмета, он остаётся выделенным. Данная команда снимает выделение."
+            ] |> String.concat "\n"
+        "unsel", dscr, unit'
+        "unselect", dscr, unit'
+        let dscr =
+            [
+                "KILLALL - эквивалентен конструкции `KILLVAR & KILLOBJ`."
+            ] |> String.concat "\n"
+        "killall", dscr, unit'
+        let dscr =
+            [
+                "`KILLVAR [$название массива],[#индекс элемента]` - удаление элемента массива. Если индекс элемента не указан, то очищается весь массив. Если оператор вызван без аргументов, то удаляются все переменные - обычно применяется в начале игры, чтобы при возврате в начальную локацию после неудачного прохождения какого-то этапа игры обнулить все переменные (в противном случае, может оказаться, что запертые двери уже открыты, жена похищена до свадьбы, а Баба-Яга уже отдала кому-то нужный клубочек). Примеры:"
+                "```qsp"
+                "KILLVAR"
+                "KILLVAR 'a'"
+                "KILLVAR 'a',3"
+                "```"
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| String; Numeric |], ()
+                [| String; |], ()
+                [||], ()
+            ] |> JustOverloads
+        "killvar", dscr, funcs
+        let dscr =
+            [
+                "`COPYARR [$массив-приёмник],[$массив-источник]` - копирование содержимого массива в другой массив. Копируются как текстовые, так и числовые значения массива. Размер массива-приёмника при копировании не имеет значения. Примеры:"
+                "```qsp"
+                "COPYARR '$a','$b'"
+                "COPYARR 'a','b'"
+                "COPYARR $arrname1,$arrname2"
+                "COPYARR 'a<<$arrname1>>','a<<$arrname2>>'"
+                "```"
+            ] |> String.concat "\n"
+        "copyarr", dscr, args [String; String]
+        let dscr =
+            [
+                "`CLEAR` или `CLR` - очистка окна пользователя."
+            ] |> String.concat "\n"
+        "clear", dscr, unit'
+        let dscr =
+            [
+                "`CLEAR` или `CLR` - очистка окна пользователя."
+            ] |> String.concat "\n"
+        "clr", dscr, unit'
+        let dscr =
+            [
+                "`CLOSE [$путь к звуковому файлу]` - остановка проигрывания звукового файла с заданным названием."
+                "`CLOSE ALL` - остановка проигрывания всех активных звуковых файлов."
+            ] |> String.concat "\n"
+        "close", dscr, arg String
+        let dscr = "`CLOSE ALL` - остановка проигрывания всех активных звуковых файлов."
+        "close all", dscr, unit' // особый-преособый случай
+        let dscr =
+            [
+                "`CLS` - эквивалентен конструкции `CLEAR & *CLEAR & CLA & CMDCLEAR`, т.е. очищает всё, кроме списка предметов."
+            ] |> String.concat "\n"
+        "cls", dscr, unit'
+        let dscr =
+            [
+                "`DYNAMIC [$строка кода],[параметр 1],[параметр 2], ...` - выполнение кода. Данный оператор позволяет динамически генерировать код игры. Переданные параметры хранятся в массиве `ARGS`. После выполнения кода предыдущие значения `ARGS` восстанавливаются. Примеры:"
+                "```qsp"
+                "DYNAMIC '$a=\"string<<$b>>\"'"
+                "DYNAMIC '$a'"
+                "DYNAMIC 'if $a=\"string\":''text!'''"
+                "DYNAMIC \""
+                "$args[0]"
+                "addobj $args[1]"
+                "\",'Текст','Вилка'"
+                "```"
+            ] |> String.concat "\n"
+        "dynamic", dscr, argAndArgList String Any
+        let dscr =
+            [
+                "`MENU [$выражение]` - вызов меню с заданным названием"
+            ] |> String.concat "\n"
+        "menu", dscr, arg String
+        let dscr =
+            [
+                "`MSG [выражение]` - вывод заданного сообщения в информационном окне."
+            ] |> String.concat "\n"
+        "msg", dscr, arg String
+        let dscr =
+            [
+                "`NL [выражение]` - переход на новую строку (перевод каретки), затем вывод текста в окне пользователя. Если `[выражение]` не указано, то перевод строки. Отличается от `PL` порядком вывода текста."
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| String |], ()
+                [| |], ()
+            ] |> JustOverloads
+        "nl", dscr, funcs
+        let dscr =
+            [
+                "`P [выражение]` - вывод текста в окно пользователя (по умолчанию находится справа внизу, обычно служит для вспомогательных целей)."
+            ] |> String.concat "\n"
+        "p", dscr, arg String
+        let dscr =
+            [
+                "`PL [выражение]` - вывод текста, затем переход на новую строку в окне пользователя. Если `[выражение]` не указано, то перевод строки"
+            ] |> String.concat "\n"
+        "pl", dscr, arg String
+        let dscr =
+            [
+                "`PLAY [$путь к звуковому файлу],[#громкость]` - проигрывание звукового файла с заданным названием и громкостью. Громкость указывается в процентах от 0 до 100."
+                "Параметр `[#громкость]` может отсутствовать, при этом громкость принимается равной 100%. Примеры:"
+                "`PLAY 'sound/music.mp3'` - проигрывает файл с громкостью 100%"
+                "`PLAY 'sound/music.mp3',50` - проигрывает файл в половину возможной громкости"
+                "`PLAY 'sound/music.mp3',0` - проигрывает файл с громкостью 0% (без звука)"
+                "`PLAY '<<$file>>.mid',volume` - проигрывает файл, имя которого хранится в $file (расширение \"mid\") с громкостью, значение которой задано в volume"
+                "`PLAY $file,volume` - аналогично"
+                "Если файл уже проигрывается, то изменяется громкость звучания без его \"перезапуска\". Поддерживается множество различных аудиоформатов и одновременное звучание до 32-х композиций."
+            ] |> String.concat "\n"
+        "play", dscr, argsAndOptional [ String ] Numeric
+        let dscr =
+            [
+                "обновление интерфейса (а также смена цветов, шрифта, назначенных с помощью системных переменных `BCOLOR`, `FCOLOR`, `LCOLOR`, `FSIZE`, `$FNAME`)."
+            ] |> String.concat "\n"
+        "refint", dscr, unit'
+        let dscr =
+            [
+                "`SETTIMER [#выражение]` - задает интервал таймера для локации-счётчика (по умолчанию 500мс, т.е. локация-счётчик обрабатывается 2 раза в секунду). Также влияет на частоту автоматического обновления интерфейса."
+            ] |> String.concat "\n"
+        "settimer", dscr, arg Numeric
+        let dscr =
+            [
+                "`SHOWACTS [#выражение]` - если значение выражения отлично от 0, то показывает список действий, иначе скрывает его. Примеры:"
+                "`SHOWACTS 1` - показывает список действий"
+                "`SHOWACTS 0` - скрывает список действий"
+            ] |> String.concat "\n"
+        "showacts", dscr, arg Numeric
+        let dscr =
+            [
+                "`SHOWINPUT [#выражение]` - если значение выражения отлично от 0, то показывает строку ввода, иначе скрывает её."
+            ] |> String.concat "\n"
+        "showinput", dscr, arg Numeric
+        let dscr =
+            [
+                "`SHOWOBJS [#выражение]` - если значение выражения отлично от 0, то показывает список предметов, иначе скрывает его."
+            ] |> String.concat "\n"
+        "showobjs", dscr, arg Numeric
+        let dscr =
+            [
+                "`SHOWSTAT [#выражение]` - если значение выражения отлично от 0, то показывает дополнительное описание, иначе скрывает его."
+            ] |> String.concat "\n"
+        "showstat", dscr, arg Numeric
+        let dscr =
+            [
+                "`VIEW [$путь к графическому файлу]` - просмотр картинки из указанного файла. Если вместо `[$путь к графическому файлу]` указана пустая строка (`''`) или параметр не указан, то это скроет окно с картинкой."
+            ] |> String.concat "\n"
+        let funcs =
+            [
+                [| String |], ()
+                [| |], ()
+            ] |> JustOverloads
+        "view", dscr, funcs
+        let dscr =
+            [
+                "`WAIT [#выражение]` - остановка выполнения программы на заданное количество миллисекунд (1 секунда = 1000 миллисекунд)."
+            ] |> String.concat "\n"
+        "wait", dscr, arg Numeric
+    ]
+let jump =
+    let dscr =
+        [
+            "`JUMP [$выражение]` - переход в текущем коде (при обработке локации / выбора действия) на метку `[$выражение]`. Метка на локации обозначается как `:[название метки]`. После описания метки (через \"&\") могут идти операторы. Если интерпретатор находит случайную метку, то он её просто игнорирует. Например:"
+            "```qsp"
+            "jump 'КонеЦ'"
+            "p 'Это сообщение не будет выведено'"
+            ":конец"
+            "p 'А это сообщение пользователь увидит'"
+            "```"
+            "С помощью оператора `JUMP` можно организовывать циклы:"
+            "```qsp"
+            "s = 0"
+            ":loop"
+            "if s < 9:"
+            "    s=s+1"
+            "    pl s"
+            "    jump 'LOOP'"
+            "end"
+            "p 'Всё!'"
+            "```"
+            "Оператор `JUMP` также полезен во время отладки квеста, чтобы \"обойти\" группу операторов, которые временно не нужны."
+        ] |> String.concat "\n"
+    "jump", dscr, arg String
+let transferOperators =
+    [
+        let dscr =
+            [
+                "`GOSUB [$выражение],[параметр 1],[параметр 2], ...` или `GS [$выражение],[параметр 1],[параметр 2], ...` - обработка локации с названием `[$выражение]`. Базовое описание локации добавляется к текущему описанию, базовые действия добавляются к текущим действиям, и происходит выполнение операторов в поле \"Выполнить при посещении\", затем возврат на исходную строку (продолжение выполнения программы)."
+                "Переданные параметры хранятся в массиве `ARGS`. После обработки локации предыдущие значения `ARGS` восстанавливаются. Примеры:"
+                "```qsp"
+                "GS 'ход' & ! обработка локации \"ход\". Массив `ARGS` пуст."
+                "GS $loc,1 & ! обработка локации, название которой хранится в $loc с передачей одного параметра. ARGS[0] равен 1."
+                "GS 'ход',$var,2,'данные' & ! обработка локации \"ход\" с передачей 3-х параметров. `$ARGS[0]` равен значению `$var`, `ARGS[1]` равен 2, `$ARGS[2]` содержит строку \"данные\"."
+                "```"
+            ] |> String.concat "\n"
+        let sign = argAndArgList String Any
+        "gosub", dscr, sign
+        "gs", dscr, sign
+        let dscr =
+            [
+                "`GOTO [$выражение],[параметр 1],[параметр 2], ...` или `GT [$выражение],[параметр 1],[параметр 2], ...` - переход на локацию с названием `[$выражение]`. Поле основного описания локации, а также список текущих действий заменяются описанием и действиями новой локации."
+                "Переданные параметры хранятся в массиве `ARGS`. Примеры:"
+                "```qsp"
+                "GT 'локация' & ! переход на локацию \"локация\". Массив `ARGS` пуст."
+                "GT 'локация',1,'данные' & ! переход на локацию \"локация\" с передачей 2-х параметров. `ARGS[0]` равен 1, `$ARGS[1]` содержит строку \"данные\"."
+                "```"
+            ] |> String.concat "\n"
+        let sign = argAndArgList String Any
+        "goto", dscr, sign
+        "gt", dscr, sign
+        let dscr =
+            [
+                "`XGOTO [$выражение],[параметр 1],[параметр 2], ...` или `XGT [$выражение],[параметр 1],[параметр 2], ...` - отличается от \"`GOTO` / `GT`\" тем, что при переходе не очищается поле основного описания локации, а базовое описание новой локации добавляется к текущему основному описанию. Тем не менее, список действий заменяется действиями новой локации."
+            ] |> String.concat "\n"
+        let sign = argAndArgList String Any
+        "xgoto", dscr, sign
+        "xgt", dscr, sign
+    ]
+let transferOperatorsSet =
+    transferOperators
+    |> List.map (fun (name, dscr, sign) -> name)
+    |> Set.ofList
+/// Всевозможные процедуры
+let procs =
+    jump::(procedures @ transferOperators)
+    |> List.map (fun (name, dscr, sign) -> name, ((dscr:Description), sign))
+    |> Map.ofList
+
+let exprSymbolicOperators =
+    let arg x (return':VarType) = arg x, return'
+    let unit' (return':VarType) = unit', return'
+    let args xs (return':VarType) = args xs, return'
+    let argList typ (return':VarType) = argList typ, return'
+    let argAndArgList x typ (return':VarType) = argAndArgList x typ, return'
+    [
+        let dscr =
+            "`[$выражение 1] & [$выражение 2]` - операция объединения строковых выражений."
+        "&", dscr, args [String; String] String
+    ]
+
+let exprNamedOperators =
+    let arg x (return':VarType) = arg x, return'
+    let unit' (return':VarType) = unit', return'
+    let args xs (return':VarType) = args xs, return'
+    let argList typ (return':VarType) = argList typ, return'
+    let argAndArgList x typ (return':VarType) = argAndArgList x typ, return'
+    [
+        // Для значения "верно" настоятельно рекомендуется использовать -1.
+        let dscr =
+            [
+                "`[#выражение 1] AND [#выражение 2]` - операция \"и\". Если оба рядом стоящие выражения верны, то верно и всё выражение."
+            ] |> String.concat "\n"
+        "and", dscr, args [Numeric; Numeric] Numeric
+        let dscr =
+            [
+                "`[#выражение 1] OR [#выражение 2]` - операция \"или\". Если хотя бы одно из рядом стоящих выражений верно, то верно и всё выражение."
+            ] |> String.concat "\n"
+        "or", dscr, args [Numeric; Numeric] Numeric
+        let dscr =
+            [
+                "`[#выражение 1] MOD [#выражение 2]` - операция вычисления остатка от деления."
+            ] |> String.concat "\n"
+        "mod", dscr, args [Numeric; Numeric] Numeric
+        let dscr =
+            [
+                "`NO [#выражение]` - отрицание. Верно, если `[#выражение]` ложно и наоборот (аналогично \"NOT\" в Basic)."
+            ] |> String.concat "\n"
+        "no", dscr, arg Numeric Numeric
+        let dscr =
+            [
+                "`OBJ [$выражение]` - верно, если в рюкзаке есть предмет `[$выражение]`."
+            ] |> String.concat "\n"
+        "obj", dscr, arg String Numeric
+        let dscr =
+            [
+                "`LOC [$выр]` - верно, если в игре есть локация с названием `[$выр]`."
+            ] |> String.concat "\n"
+        "loc", dscr, arg String Numeric
+    ]
+let keywords =
+    [
+        let dscr =
+            [
+                "`IF [#выражение]:[оператор1] & [оператор2] & ... ELSE [оператор3] & [оператор4] & ...` - если `[#выражение]` верно (не равно 0), то выполнить заданные операторы до ключевого слова `ELSE`, иначе выполнить операторы после `ELSE`."
+                "Если ключевое слово `ELSE` не указано, то при верном значении [#выражение], выполняются все операторы, находящиеся после символа `:`."
+                "Примеры:"
+                "```qsp"
+                "if ((a+b)/c)=45+54 or (b<5 or c>45) and no obj 'лопата' and $f=$vvv+'RRRRR':p 'OK' & goto 'Next'"
+                "if был_здесь[$curloc]:exit"
+                "if a<3:jump 'sss'"
+                "if $имя = '':msg 'Введите имя!' & jump 'ввод'"
+                "if a+b=2:c=30 & gt 'next' else c=10"
+                "```"
+            ] |> String.concat "\n"
+        "if", dscr
+        "else", dscr
+        "elseif", dscr
+        "end", "Завершает конструкции `ACT`, `IF` или `FOR`."
+        let dscr =
+            [
+                "`ACT [$название],[$путь к файлу изображения]:[оператор] & [оператор] & ...` - добавление действия к существующим на локации."
+                "К действиям добавляется новое с описанием `[$название]` и изображением `[$путь к файлу изображения]`. При нажатии на него выполнятся заданные операторы."
+                "Параметр `[$путь к файлу изображения]` может отсутствовать, при этом действие добавится без изображения."
+            ] |> String.concat "\n"
+        "act", dscr
+        let dscr =
+            [
+                "`SET [название переменной]=[выражение]`, `LET [название переменной]=[выражение]` или `[название переменной]=[выражение]` - установка значения переменной. Если нужно установить текстовое значение переменной, то перед её названием ставится `$`."
+                "Примеры:"
+                "```qsp"
+                "SET A=123"
+                "SET $B='строка'"
+                "LET C=A"
+                "D=456"
+                "$D='ещё строка'"
+                "$D='и ещё"
+                "       одна"
+                "строка'"
+                "```"
+            ] |> String.concat "\n"
+        "set", dscr
+        "let", dscr
+        let dscr =
+            [
+                "завершение выполнения текущего кода (преждевременный выход из подпрограммы / обработчика какого-либо события...)."
+            ] |> String.concat "\n"
+        "exit", dscr
+        let dscr =
+            [
+                "**FOR** `[#переменная]` **=** `[#выражение]` **TO** `[#выражение]`**:** `[операторы]` - Выполняет `[#операторы]` несколько раз, по очереди присваивая `[#переменной]` все численные значения от первого до второго `[#выражения]`."
+                ""
+                "Однострочная форма записи:"
+                "```qsp"
+                "for номер_нпц = 1 to количество_нпц: gs 'инициализировать нпц', номер_нпц"
+                "стоимость['меч'] = 10"
+                "стоимость['доспех'] = 250"
+                "стоимость['щит'] = 15"
+                "стоимость_снаряжения = 0"
+                "for номер_предмета = 0 to arrsize('стоимость')-1: стоимость_снаряжения += стоимость[номер предмета]"
+                "```"
+                ""
+                "Многострочная форма записи:"
+                "* После символа `:` ставится перенос строки"
+                "* Заканчивается FOR строкой `END`"
+                "* Допускается вложенность неограниченной глубины. Каждый уровень вложения должен заканчиваться своей строкой `END`."
+                "* Пример:"
+                "    ```qsp"
+                "    for i = 0 to arrsize('arr')-1:"
+                "        *pl arr[i]"
+                "        if arr[i] > 10:"
+                "            jump конец"
+                "        end"
+                "    end"
+                "    ```"
+                ""
+                "Можно еще задать шаг цикла, для этого используется **STEP**:"
+                "```qsp"
+                "for нечётные = 1 to 10 step 2: *pl нечётные"
+                "```"
+            ] |> String.concat "\n"
+        "for", dscr
+        "to", "**TO** — ключевое слово для конструкции FOR"
+    ]

+ 455 - 0
QSParse/Parsec.fs

@@ -0,0 +1,455 @@
+module Qsp.Parser.Main
+open FParsec
+open FsharpMyExtension
+open FsharpMyExtension.Either
+open Qsp
+
+open Qsp.Ast
+open Qsp.Parser.Generic
+open Qsp.Parser.Expr
+open Qsp.Tokens
+
+let ppunctuationTerminator : _ Parser =
+    appendToken TokenType.KeywordControl (pchar '&')
+
+let pImplicitVarWhenAssign p =
+    applyRange p
+    >>= fun (range, (name:string)) ->
+        let nameLower = name.ToLower()
+        Defines.vars
+        |> Map.tryFind nameLower
+        |> function
+            | Some dscr ->
+                appendHover2 dscr range
+            | None ->
+                if Map.containsKey nameLower Defines.procs then
+                    appendSemanticError range "Нельзя переопределять процедуру"
+                elif Map.containsKey nameLower Defines.functions then
+                    appendSemanticError range "Нельзя переопределять функцию"
+                else
+                    let dscr = "Пользовательская глобальная переменная числового типа"
+                    appendHover2 dscr range
+        >>. appendToken2 TokenType.Variable range
+        >>. appendVarHighlight range (ImplicitNumericType, name) VarHighlightKind.WriteAccess
+        >>. preturn name
+
+let pAssign stmts =
+    let assdef name ass =
+        let asscode =
+            between (pchar '{' >>. spaces) (spaces >>. char_ws '}') stmts
+            |>> fun stmts -> AssignCode(ass, stmts)
+
+        let call =
+            ident >>=?
+            fun name ->
+                followedBy (
+                    ident
+                    <|> (puint32 >>% "")
+                    <|> stringLiteral)
+                >>. sepBy1 pexpr (char_ws ',')
+                |>> fun args -> Assign(ass, Func(name, args)) // То есть `a = min x, y` можно, что ли? Хм...
+        let assexpr = call <|> (pexpr |>> fun defExpr -> Assign(ass, defExpr))
+
+        let str_ws s =
+            appendToken TokenType.OperatorAssignment
+                (pstring s)
+            .>> ws
+
+        choice [
+            str_ws "-=" >>. pexpr |>> fun defExpr -> Assign(ass, Expr.Expr(Minus, Var name, defExpr))
+            str_ws "=-" >>. pexpr |>> fun defExpr -> Assign(ass, Expr.Expr(Minus, defExpr, Var name))
+            (str_ws "+=" <|> str_ws "=+") >>. pexpr |>> fun defExpr -> Assign(ass, Expr.Expr(Plus, Var name, defExpr))
+            str_ws "=" >>. (asscode <|> assexpr)
+        ]
+
+    let assign name =
+        let arr =
+            bet_ws '[' ']' (opt pexpr)
+            |>> fun braketExpr ->
+                match braketExpr with
+                | Some braketExpr ->
+                    AssignArr(name, braketExpr)
+                | None -> AssignArrAppend name
+        arr <|>% AssignVar name >>=? assdef name
+    let pExplicitAssign =
+        let p =
+            appendToken
+                TokenType.Type
+                ((pstringCI "set" <|> pstringCI "let") .>>? notFollowedVarCont)
+            .>> ws
+            >>. (pexplicitVar VarHighlightKind.WriteAccess <|> (pImplicitVarWhenAssign ident |>> fun name -> ImplicitNumericType, name))
+        p <|> pexplicitVar VarHighlightKind.WriteAccess .>>? ws
+        >>=? assign
+
+    let pImlicitAssign =
+        pImplicitVarWhenAssign notFollowedByBinOpIdent .>>? ws
+        >>=? fun name ->
+            assign (ImplicitNumericType, name)
+    pExplicitAssign <|> pImlicitAssign
+
+let pcallProc =
+    let f defines p =
+        applyRange p
+        >>= fun (range, name) ->
+            let p =
+                defines
+                |> Map.tryFind (String.toLower name)
+                |> function
+                    | Some (dscr, sign) ->
+                        appendHover2 dscr range
+                        >>% Some sign
+                    | None ->
+                        [
+                            "Такой процедуры нет, а если есть, то напишите мне, автору расширения, пожалуйста, и я непременно добавлю."
+                            "Когда-нибудь добавлю: 'Возможно, вы имели ввиду: ...'"
+                        ]
+                        |> String.concat "\n"
+                        |> appendSemanticError range
+                        >>% None
+            appendToken2 TokenType.Procedure range
+            >>. p
+            |>> fun sign -> name, range, sign
+    let pProcWithAsterix: _ Parser =
+        let p =
+            pchar '*' >>. many1Satisfy isIdentifierChar
+            |>> sprintf "*%s" // да, так и хочется использоваться `many1Satisfy2`, но она довольствуется лишь первым символом, то есть '*', потому не подходит
+
+        f Defines.proceduresWithAsterix p
+
+    let procHoverAndToken =
+        f Defines.procs notFollowedByBinOpIdent
+
+    let pDefProc : _ Parser =
+        Defines.procs
+        |> Seq.sortByDescending (fun (KeyValue(name, _)) -> name) // для жадности
+        |> Seq.map (fun (KeyValue(name, (dscr, sign))) ->
+            applyRange (pstringCI name .>>? notFollowedVarCont)
+            >>= fun (range, name) ->
+                appendToken2 TokenType.Procedure range
+                >>. appendHover2 dscr range
+                >>% (name, range, sign)
+        )
+        |> List.ofSeq
+        |> choice
+    /// Особый случай, который ломает к чертям весь заявленный синтаксис
+    let adhoc =
+        let createIdent name =
+            pstringCI name .>>? notFollowedVarCont
+        let p name name2 =
+            createIdent name .>>? ws1 .>>.? createIdent name2
+        applyRange
+            ((p "add" "obj"
+              <|> (createIdent "del" .>>? ws1 .>>.? (createIdent "obj" <|> createIdent "act"))
+              |>> fun (name1, name2) -> name1 + name2)
+             <|> (p "close" "all" |>> fun (name1, name2) -> sprintf "%s %s" name1 name2))
+        >>= fun (range, name) ->
+            match Map.tryFind (String.toLower name) Defines.procs with
+            | Some (dscr, sign) ->
+                appendToken2 TokenType.Procedure range
+                >>. appendHover2 dscr range
+                >>% (name, range, sign)
+            | None -> failwithf "'%s' not found in predef procs" name
+    pProcWithAsterix
+    .>> ws .>>. sepBy (applyRange pexpr) (char_ws ',') // Кстати, `,` — "punctuation.separator.parameter.js"
+    <|> (adhoc <|> pDefProc .>> ws
+         .>>. (followedBy (skipNewline <|> skipChar '&' <|> eof) >>% []
+               <|> bet_ws '(' ')' (sepBy (applyRange pexpr) (pchar ',' >>. ws))
+               <|> sepBy1 (applyRange pexpr) (char_ws ','))
+         |>> fun ((name, range, sign), args) -> ((name, range, Some sign), args))
+    <|> (procHoverAndToken
+         .>>? (ws1 <|> followedBy (satisfy (isAnyOf "'\"")))
+         .>>.? sepBy1 (applyRange pexpr) (char_ws ','))
+    >>= fun ((name, range, sign), args) ->
+        match sign with
+        | None ->
+            preturn (CallSt(name, List.map snd args))
+        | Some x ->
+            let procNameLower = String.toLower name
+            let pLoc =
+                if Set.contains procNameLower Defines.transferOperatorsSet then
+                    match args with
+                    | (r, Val (String [[StringKind locName]]))::_ ->
+                        getUserState
+                        >>= fun (x:State) ->
+                        let nested =
+                            if x.SingleQuotNestedCount > x.DoubleQuotNestedCount then // TODO: ничего хорошего из этого не получится
+                                x.SingleQuotNestedCount
+                            else
+                                x.DoubleQuotNestedCount
+                            |> (+) 1
+                        let r =
+                            { r with
+                                Column1 = r.Column1 + int64 nested // чтобы `'` или `"` пропустить
+                                Column2 = r.Column2 - int64 nested
+                            }
+                        let locNameLower = String.toLower locName
+                        appendLocHighlight r locNameLower VarHighlightKind.ReadAccess
+                        >>. pGetDefLocPos locNameLower
+                            >>= function
+                                | None ->
+                                    updateUserState (fun st ->
+                                        { st with
+                                            NotDefinedLocs =
+                                                st.NotDefinedLocs
+                                                |> Map.addOrMod locNameLower [r] (fun xs -> r::xs)
+                                        }
+                                    )
+                                | Some _ -> preturn ()
+                    | _ -> preturn ()
+                else
+                    preturn ()
+            args
+            |> Array.ofList
+            |> Defines.getFuncByOverloadType x
+            |> function
+                | None ->
+                    let msg =
+                        Defines.Show.printSignature name x
+                        |> sprintf "Ожидается одна из перегрузок:\n%s"
+                    appendSemanticError range msg
+                | Some () ->
+                    preturn ()
+            >>. pLoc
+            >>% CallSt(name, List.map snd args)
+
+let pcomment : _ Parser =
+    let stringLiteralWithToken : _ Parser =
+        let bet tokenType openedChar closedChar =
+            let p =
+                many1Satisfy (fun c' -> not (c' = closedChar || c' = '\n'))
+                <|> (attempt(skipChar closedChar >>. skipChar closedChar)
+                      >>% string closedChar + string closedChar)
+            pipe2
+                (appendToken tokenType (pchar openedChar)
+                 >>. appendToken tokenType (manyStrings p))
+                (many
+                    (newline >>. appendToken tokenType (manyStrings p))
+                 .>> appendToken tokenType (pchar closedChar)) // TODO: Здесь самое то использовать `PunctuationDefinitionStringEnd`
+                (fun x xs ->
+                    x::xs |> String.concat "\n"
+                    |> fun x -> sprintf "%c%s%c" openedChar x closedChar
+                    )
+        bet TokenType.Comment '\'' '\''
+        <|> bet TokenType.Comment '"' '"'
+    let p =
+        appendToken TokenType.Comment
+            (many1Satisfy (fun c -> c <> '\n' && c <> ''' && c <> '"' && c <> '{'))
+        <|> stringLiteralWithToken
+        <|> (pbraces TokenType.Comment |>> sprintf "{%s}")
+    appendToken TokenType.Comment (pchar '!')
+    >>. manyStrings p |>> Statement.Comment
+
+let psign =
+    appendToken TokenType.LabelColon
+        (pchar ':')
+    >>. ws
+    >>. appendToken TokenType.NameLabel
+            (many1SatisfyL ((<>) '\n') "labelName") // TODO: literal? Trim trailing spaces
+    |>> Label
+let genKeywordParser keyword =
+    let dscr =
+        Qsp.Defines.keywords
+        |> List.tryPick (fun (name, dscr) ->
+            if name = keyword then Some dscr
+            else None)
+        |> Option.defaultWith (fun () -> failwithf "not found %s" keyword)
+    appendTokenHover TokenType.KeywordControl dscr
+        (pstringCI keyword .>>? notFollowedVarCont)
+let pexit : _ Parser =
+    genKeywordParser "exit"
+    >>% Exit
+
+let pendKeyword : _ Parser =
+    genKeywordParser "end"
+let pstmts' pstmt =
+    many
+        (pstmt .>> spaces
+         .>> (skipMany (ppunctuationTerminator .>> spaces)))
+let pstmts1' pstmt =
+    many1
+        (pstmt .>> spaces
+         .>> (skipMany (ppunctuationTerminator .>> spaces)))
+let pstmt =
+    let pstmt, pstmtRef = createParserForwardedToRef<Statement, _>()
+    let pInlineStmts =
+        many (pstmt .>> ws .>> skipMany (ppunctuationTerminator .>> ws))
+    let pInlineStmts1 =
+        many1 (pstmt .>> ws .>> skipMany (ppunctuationTerminator .>> ws))
+    let pstmts = pstmts' pstmt
+
+    let pcolonKeyword : _ Parser =
+        appendToken TokenType.KeywordControl (pchar ':')
+
+    let pAct =
+        let pactKeyword : _ Parser =
+            genKeywordParser "act"
+
+        let pactHeader = pactKeyword .>> ws >>. sepBy1 pexpr (char_ws ',') .>> pcolonKeyword
+
+        pipe2
+            pactHeader
+            ((ws >>? skipNewline >>. spaces >>. pstmts .>> pendKeyword)
+              <|> (spaces >>. pInlineStmts .>> optional pendKeyword))
+            (fun expr body ->
+                Act(expr, body))
+    let pFor =
+        let pForHeader =
+            genKeywordParser "for" >>. ws
+            >>. (pexplicitVar VarHighlightKind.WriteAccess
+                 <|> (pImplicitVarWhenAssign ident |>> fun name -> ImplicitNumericType, name))
+            .>> ws .>> appendToken TokenType.OperatorAssignment (pchar '=')
+            .>> ws .>>. pexpr
+            .>> genKeywordParser "to"
+            .>> ws .>>. pexpr
+            .>> pcolonKeyword
+
+        pipe2
+            pForHeader
+            ((ws >>? skipNewline >>. spaces >>. pstmts .>> pendKeyword)
+              <|> (spaces >>. pInlineStmts .>> optional pendKeyword))
+            (fun ((var, fromExpr), toExpr) body ->
+                For(var, fromExpr, toExpr, body))
+    let pIf =
+        let pifKeyword : _ Parser =
+            genKeywordParser "if"
+        let pelseifKeyword : _ Parser =
+            genKeywordParser "elseif"
+        let pelseKeyword : _ Parser =
+            genKeywordParser "else"
+        let pifHeader = pifKeyword .>> ws >>. pexpr .>> pcolonKeyword
+        let pelseifHeader = pelseifKeyword .>> ws >>. pexpr .>> pcolonKeyword
+
+        let setIsEndOptionalTo boolean =
+            updateUserState (fun x -> { x with IsEndOptional = boolean })
+
+        let p =
+            ws .>>? skipNewline >>. spaces >>. pstmts .>> setIsEndOptionalTo false
+            <|> (spaces >>. pInlineStmts .>> setIsEndOptionalTo true)
+        let pElse1 =
+            pelseKeyword >>. ws
+            >>. (pInlineStmts1 .>> opt pendKeyword
+                 <|> (spaces >>. pstmts .>> pendKeyword))
+        let pend =
+            getUserState
+            >>= fun x ->
+                if x.IsEndOptional then
+                    optional pendKeyword
+                else
+                    pendKeyword >>% ()
+
+        let pelseIf =
+            many1 (pelseifHeader .>>. p)
+            .>>. (pElse1 <|> (pend >>% []))
+            |>> fun (elifs, elseBody) ->
+                let rec f = function
+                    | (expr, thenBody)::xs ->
+                        [If(expr, thenBody, f xs)]
+                    | [] -> elseBody
+                f elifs
+
+        // `end` нужен, чтобы инструкции, определенные ниже, не ушли в тело `if`
+        // ```qsps
+        // if expr:
+        //     stmt1
+        // end & ! без него `stmt2` станет принадлежать телу `if`
+        // stmt2
+        // ...
+        // ```
+
+        // `if expr: stmt1 & stmt2 & ...` — такому выражению `end` не нужен, поскольку эту роль выполняет конец строки.
+        // Также работает и с `elif expr: stmt1 & stmt2 & ...`, и с `else expr: stmt1 & stmt2 & ...`.
+        pipe2
+            (pifHeader .>> ws)
+            ((pInlineStmts1 .>>. (pelseIf <|> pElse1 <|> (opt pendKeyword >>% []))
+             <|> (spaces >>. pstmts .>>. (pelseIf <|> pElse1 <|> (pendKeyword >>% [])))))
+            (fun expr (thenBody, elseBody) ->
+                If(expr, thenBody, elseBody))
+    pstmtRef :=
+        choice [
+            pcomment
+            pexit
+            psign
+            pIf
+            pAct
+            pFor
+            pAssign pstmts
+            pcallProc
+            notFollowedBy (pchar '-' >>. ws >>. (skipNewline <|> skipChar '-' <|> eof)) // `-` завершает локацию
+            >>. (pexpr |>> StarPl)
+        ]
+    pstmt
+
+let pstmts = pstmts' pstmt
+let pstmts1 = pstmts1' pstmt
+
+let psharpKeyword : _ Parser =
+    appendToken TokenType.KeywordControl (pchar '#')
+let pminusKeyword : _ Parser =
+    appendToken TokenType.KeywordControl (pchar '-') // хотя здесь больше подошел бы обычный `end`
+let ploc =
+    let pendKeyword =
+        applyRange (pstringCI "end" .>>? notFollowedVarCont)
+        >>= fun (range, _) ->
+            appendToken2 TokenType.KeywordControl range
+            >>. appendSemanticError range "Лишний `end`"
+    pipe2
+        (psharpKeyword .>> ws
+         >>. (applyRange
+                (many1Strings
+                    (many1Satisfy (isNoneOf " \t\n")
+                     <|> (many1Satisfy (isAnyOf " \t") .>>? notFollowedByNewline))
+                 <?> "location name")
+              >>= fun (r, name) ->
+                let pCheckLocExists r2 locName =
+                    pGetDefLocPos locName
+                    >>= function
+                        | Some r ->
+                            sprintf "Локация уже определена в\n%A" r
+                            |> appendSemanticError r2
+                        | None -> preturn ()
+
+                let locNameLower = String.toLower name
+                pCheckLocExists r locNameLower
+                >>. updateUserState (fun st ->
+                        { st with
+                            NotDefinedLocs =
+                                Map.remove locNameLower st.NotDefinedLocs // ну да, к чему проверки? И так удалит
+                        }
+                    )
+                >>. appendLocHighlight r locNameLower VarHighlightKind.WriteAccess // и все равно добавить, даже в случае семантической ошибки? Хм, ¯\_(ツ)_/¯
+                >>. appendToken2 TokenType.StringQuotedSingle r
+                >>. preturn name
+             )
+         .>> spaces)
+        (many (pstmts1 .>> many (pendKeyword .>> spaces)) |>> List.concat
+         .>> (pminusKeyword .>> ws
+              .>> appendToken TokenType.Comment
+                    (skipManySatisfy ((<>) '\n'))))
+        (fun name body -> Location(name, body))
+
+let pAfterAll =
+    preturn ()
+let start str =
+    let emptyState =
+        { emptyState with PStmts = pstmts }
+    let p =
+        spaces >>. many (ploc .>> spaces)
+        .>> (getPosition >>= fun p ->
+                updateUserState (fun st ->
+                    { st with LastSymbolPos = p}))
+    runParserOnString (p .>> pAfterAll .>> eof)
+        emptyState
+        ""
+        str
+let startOnFile enc path =
+    let emptyState =
+        { emptyState with PStmts = pstmts }
+    let p =
+        spaces >>. many (ploc .>> spaces)
+        .>> (getPosition >>= fun p ->
+                updateUserState (fun st ->
+                    { st with LastSymbolPos = p}))
+    runParserOnFile (p .>> pAfterAll .>> eof)
+        emptyState
+        path
+        enc

+ 285 - 0
QSParse/ParserExpr.fs

@@ -0,0 +1,285 @@
+module Qsp.Parser.Expr
+open FParsec
+open FsharpMyExtension
+open FsharpMyExtension.Either
+open Qsp
+open Qsp.Ast
+open Qsp.Parser.Generic
+open Qsp.Tokens
+
+let pbinaryOperator : _ Parser =
+    [
+        Defines.exprNamedOperators |> List.map (fun (x, _, _) -> x)
+        Defines.keywords |> List.map fst
+    ]
+    |> List.concat
+    |> List.sortDescending
+    |> List.map pstringCI
+    |> choice
+
+/// берёт только то, что начинается с `#` или `$`
+let pexplicitVar varHighlightKind : _ Parser =
+    let isIdentifierFirstChar c = isLetter c || c = '_'
+    let p = many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier"
+    // TODO: или просто `many1Satisfy isIdentifierChar` ?
+    let varType =
+        choice [
+            pchar '#' >>% ExplicitNumericType
+            pchar '$' >>% StringType
+        ]
+
+    (getPosition .>>.? varType) .>>. (p .>>. getPosition)
+    >>= fun ((p1, typ), (varName, p2)) ->
+        let range = toRange p1 p2
+        let msg =
+            match typ with
+            | StringType ->
+                Defines.vars
+                |> Map.tryFind (sprintf "$%s" (varName.ToLower()))
+                |> function
+                    | Some dscr -> dscr
+                    | None -> "Пользовательская глобальная переменная строчного типа"
+            | ExplicitNumericType ->
+                Defines.vars
+                |> Map.tryFind (sprintf "#%s" (varName.ToLower()))
+                |> function
+                    | Some dscr -> dscr
+                    | None -> "Пользовательская глобальная переменная числового типа"
+            | ImplicitNumericType -> failwith "Not Implemented"
+        appendToken2 Tokens.Variable range
+        >>. appendHover2 msg range
+        >>. appendVarHighlight range (typ, varName) varHighlightKind
+        >>. preturn (typ, varName)
+type ProcOrFunc =
+    | Procedure of string
+    | Function of string
+
+let notFollowedByBinOpIdent =
+    // конечно, тут нужно объяснить пользователю, почему именно нельзя использовать то или иное слово
+    // проще назвать, что допустимо
+    // let p =
+    //     choice [
+    //         spaces1
+    //         skipChar '"'
+    //         skipChar '''
+    //         eof
+    //     ]
+    // let followedVarCont =
+    //     followedBy (satisfy (fun c -> isDigit c || c = '_' || c = '.'))
+    let p =
+        pbinaryOperator
+        .>> (skipSatisfy (not << isIdentifierChar)
+             <|> eof)
+    let p2 =
+        notFollowedByL p "идентификатор, а не строковый бинарный оператор"
+        >>. ident
+    // runStateEither p2 emptyState "no"
+    // runStateEither p2 emptyState "no " // нельзя
+    // runStateEither p2 emptyState "node" // можно
+    // runStateEither p2 emptyState "foobar" // можно
+    p2
+
+let term expr =
+    let pcallFuncOrArrOrVar =
+        let pbraket = bet_ws '[' ']' (sepBy expr (skipChar ',' >>. ws))
+        let pexplicitVar =
+            pexplicitVar VarHighlightKind.ReadAccess .>> ws .>>. opt pbraket
+            |>> fun (var, arr) ->
+                match arr with
+                | Some args -> Arr(var, args)
+                | None -> Var var
+        let pBracesArgs =
+            bet_ws '(' ')' (sepBy expr (pchar ',' >>. ws))
+        let pcallFunctionOrArrOrVar =
+            tuple2
+                (applyRange notFollowedByBinOpIdent
+                 .>> ws)
+                ((pBracesArgs |>> fun args -> TokenType.Function, fun name -> Func(name, args))
+                  <|> (pbraket
+                       |>> fun arg ->
+                            let f name = Arr((ImplicitNumericType, name), arg)
+                            TokenType.Variable, f)
+                  <|>% (TokenType.Variable, fun name -> Var(ImplicitNumericType, name)))
+            >>= fun ((range, name), (tokenType, f)) ->
+                    match tokenType with
+                    | TokenType.Function ->
+                        match f name with
+                        | Func(name, args) as func ->
+                            let p =
+                                [
+                                    "Такой функции нет, а если есть, то напишите мне, автору расширения, пожалуйста, и я непременно добавлю."
+                                    "Когда-нибудь добавлю: 'Возможно, вы имели ввиду: ...'"
+                                ]
+                                |> String.concat "\n"
+                                |> appendSemanticError range
+                            p
+                            >>. appendToken2 tokenType range
+                            >>% func
+                        | func -> preturn func
+                    | TokenType.Variable ->
+                        let p =
+                            Defines.vars
+                            |> Map.tryFind (name.ToLower())
+                            |> function
+                                | Some dscr ->
+                                    appendHover2 dscr range
+                                | None ->
+                                    let dscr = "Пользовательская глобальная переменная числового типа"
+                                    appendHover2 dscr range
+                        p
+                        >>. appendToken2 tokenType range
+                        >>. appendVarHighlight range (ImplicitNumericType, name) VarHighlightKind.ReadAccess
+                        >>% f name
+                    | tokenType ->
+                        appendToken2 tokenType range
+                        >>% f name
+        let pPreDefFunc =
+            Defines.functions
+            |> Seq.sortByDescending (fun (KeyValue(name, _)) -> name) // для жадности
+            |> Seq.map (fun (KeyValue(name, (dscr, sign))) ->
+                applyRange (pstringCI name .>>? notFollowedVarCont)
+                >>= fun (range, name) ->
+                    appendToken2 TokenType.Function range
+                    >>. appendHover2 dscr range
+                    >>% (name, range, sign)
+            )
+            |> List.ofSeq
+            |> choice
+        pPreDefFunc .>> ws .>>. (opt pBracesArgs |>> Option.defaultValue [])
+        >>= fun ((name, range, (sign, returnType)), args) ->
+                let p =
+                    args
+                    |> Array.ofList
+                    |> Defines.getFuncByOverloadType sign
+                    |> function
+                        | None ->
+                            let msg =
+                                Defines.Show.printFuncSignature name returnType sign
+                                |> sprintf "Ожидается одна из перегрузок:\n%s"
+                            appendSemanticError range msg
+                        | Some () ->
+                            preturn ()
+                p
+                >>% Func(name, args)
+        <|> pexplicitVar
+        <|> pcallFunctionOrArrOrVar
+    let pval =
+        choice [
+            // TODO: `pbraces` — он точно нужен?
+            stringLiteralWithToken expr |>> String
+            appendToken TokenType.ConstantNumericInteger
+                (pint32 |>> Int)
+        ]
+        |>> Val
+    pval <|> pcallFuncOrArrOrVar <|> bet_ws '(' ')' expr
+
+let pExprOld : _ Parser =
+    let opp = new OperatorPrecedenceParser<Expr, unit, _>()
+
+    let expr = opp.ExpressionParser
+    opp.TermParser <- term expr .>> ws
+
+    Op.ops
+    |> Array.iter (fun (opTyp, (opName, isSymbolic)) ->
+        let prec = Precedences.prec <| Precedences.OpB opTyp
+        if isSymbolic then
+            if opName = ">" then
+                // внутри string есть подстановка `<<expr>>`, и эта условность нужна, чтобы не захватывать `>>`
+                let p = notFollowedBy (pchar '>') >>. ws
+                InfixOperator(opName, p, prec, Associativity.Left, fun x y -> Expr(opTyp, x, y))
+            else
+                InfixOperator(opName, ws, prec, Associativity.Left, fun x y -> Expr(opTyp, x, y))
+            |> opp.AddOperator
+        else
+            let afterStringParser = notFollowedVarCont >>. ws
+            InfixOperator(opName, afterStringParser, prec, Associativity.Left, fun x y -> Expr(opTyp, x, y))
+            |> opp.AddOperator
+            InfixOperator(opName.ToUpper(), afterStringParser, prec, Associativity.Left, fun x y -> Expr(opTyp, x, y))
+            |> opp.AddOperator
+    )
+
+    Reflection.Reflection.unionEnum
+    |> Array.iter (fun unT ->
+        let afterStringParser opName =
+            if String.forall isLetter opName then
+                notFollowedVarCont
+                >>. ws
+            else
+                ws
+        let unarOp = UnarOp.toString unT
+        let prec = Precedences.prec <| Precedences.PrefB unT
+        PrefixOperator(unarOp, afterStringParser unarOp, prec, false, fun x -> UnarExpr(unT, x))
+        |> opp.AddOperator
+    )
+    expr <?> "expr"
+
+let pExprNew : _ Parser =
+    let pExpr, pExprRef = createParserForwardedToRef()
+    let term = term pExpr
+    let pchar c typ =
+        appendToken typ (pchar c)
+    let pstringCI c typ =
+        appendToken typ (pstringCI c)
+    let pstring c typ =
+        appendToken typ (pstring c)
+
+    let pNeg =
+        pchar '-' TokenType.OperatorArithmetic >>. ws >>. term
+        |>> fun e1 -> UnarExpr(Neg, e1)
+    let pProd =
+        chainl1 (pNeg <|> term .>> ws)
+            ((pchar '*' TokenType.OperatorArithmetic >>% Times
+              <|> (pchar '/' TokenType.OperatorArithmetic >>% Divide))
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+    let pMod =
+        chainl1 (pProd .>> ws)
+            ((pstringCI "mod" TokenType.OperatorArithmetic >>? notFollowedVarCont >>. ws >>% Mod)
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+    let pSum =
+        chainl1 (pMod .>> ws)
+            ((pchar '+' TokenType.OperatorArithmetic >>% Plus
+              <|> (pchar '-' TokenType.OperatorArithmetic >>% Minus))
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+    let pCompare pNo =
+        chainl1 (pNo <|> pSum .>> ws)
+            (choice [
+                pstring "=>" TokenType.OperatorComparison >>% Eg
+                pstring "=<" TokenType.OperatorComparison >>% El
+                pchar '=' TokenType.OperatorRelational >>% Eq
+
+                pstring "<>" TokenType.OperatorRelational >>% Ne
+                pstring "<=" TokenType.OperatorComparison >>% Le
+                pchar '<' TokenType.OperatorComparison >>% Lt
+
+                pstring ">=" TokenType.OperatorComparison >>% Ge
+                pchar '>' TokenType.OperatorComparison .>>? notFollowedBy (FParsec.CharParsers.pchar '>') >>% Gt // чтобы исключить `>>`
+
+                pchar '!' TokenType.OperatorRelational >>% Bang
+             ]
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+    let pObj pNo =
+        let pObj =
+            pstringCI "obj" TokenType.Procedure .>>? notFollowedVarCont >>% Obj
+            <|> (pstringCI "loc" TokenType.Procedure .>>? notFollowedVarCont >>% Loc)
+            .>> ws .>>. pCompare pNo
+            |>> fun (op, e1) -> UnarExpr(op, e1)
+        pObj <|> pCompare pNo .>> ws
+    let pNo =
+        // TODO: `no` — ассоциативный оператор, потому допустимо такое: `no (no -1)`
+        let pNo, pNoRef = createParserForwardedToRef()
+        pNoRef :=
+            pstringCI "no" TokenType.Procedure >>? notFollowedVarCont >>. ws >>. pObj pNo
+            |>> fun e1 -> UnarExpr(No, e1)
+        pNo <|> pObj pNo .>> ws
+    let pAnd =
+        chainl1 (pNo .>> ws)
+            ((pstringCI "and" TokenType.Procedure >>? notFollowedVarCont >>. ws >>% And)
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+    let pOr =
+        chainl1 (pAnd .>> ws)
+            ((pstringCI "or" TokenType.Procedure >>? notFollowedVarCont >>. ws >>% Or)
+             .>> ws |>> fun op e1 e2 -> Expr(op, e1, e2))
+
+    pExprRef := pOr
+    pExpr
+let pexpr = pExprNew

+ 389 - 0
QSParse/ParserGeneric.fs

@@ -0,0 +1,389 @@
+module Qsp.Parser.Generic
+open FParsec
+open FsharpMyExtension
+open FsharpMyExtension.Either
+open Qsp
+
+let runEither p str =
+    match run p str with
+    | Success(x, _, _) -> Right x
+    | Failure(x, _, _) -> Left x
+let runStateEither p st str =
+    match runParserOnString p st "" str with
+    | Success(x, st, _) -> st, Right(x)
+    | Failure(x, _, st) -> st, Left(x)
+let isIdentifierChar c = isLetter c || isDigit c || c = '_' || c = '.'
+
+let ident<'UserState> =
+    skipChar '_' >>? many1Satisfy isIdentifierChar
+    |>> fun ident -> "_" + ident
+    <|> many1Satisfy2L isLetter isIdentifierChar "identifier"
+    : Parser<_, 'UserState>
+
+let ws<'UserState> =
+    skipManySatisfy (fun c -> System.Char.IsWhiteSpace c && c <> '\n')
+    : Parser<_, 'UserState>
+let ws1<'UserState> =
+    skipMany1SatisfyL (fun c -> System.Char.IsWhiteSpace c && c <> '\n') "any white space except '\\n'"
+    : Parser<_, 'UserState>
+
+let char_ws c = pchar c .>> ws
+let bet opened closed = between <| char_ws opened <| pchar closed
+let bet_ws opened closed p = bet opened closed p .>> ws
+let optList p = p <|>% []
+
+let nl<'UserState> = skipMany1 newline : Parser<unit, 'UserState>
+
+let stringLiteral<'UserState> =
+    let normalChar c = satisfy (fun c' -> c' <> c)
+    let p c = manyChars (normalChar c <|> attempt(pchar c >>. pchar c))
+
+    let bet openedChar closedChar = between (pchar openedChar) (pchar closedChar)
+    bet '"' '"' (p '"')
+    <|> bet '\'' '\'' (p '\'')
+    <|> bet '{' '}' (p '}') // TODO: забавно: проверил компилятор, и тот напрочь не воспринимает экранирование `}}`
+    : Parser<_, 'UserState>
+
+
+/// Дело в том, что названия переменных могут начинаться с ключевых слов ("**if**SomethingTrue", например), а значит, чтобы это пресечь, можно воспользоваться именно этой функцией так:
+/// ```fsharp
+/// pstring "if" .>>? notFollowedVar
+/// ```
+let notFollowedVarCont<'UserState> =
+    notFollowedBy (satisfy isIdentifierChar)
+    : Parser<_, 'UserState>
+
+/// A document highlight kind.
+[<RequireQualifiedAccess>]
+type DocumentHighlightKind =
+    /// A textual occurrence.
+    | Text = 1
+
+    /// Read-access of a symbol, like reading a variable.
+    | Read = 2
+
+    /// Write-access of a symbol, like writing to a variable.
+    | Write = 3
+type VarHighlightKind =
+    | ReadAccess
+    | WriteAccess
+// type Var =
+type VarHighlights =
+    {
+        Ma: Map<Ast.Var, (Tokens.InlineRange * VarHighlightKind) list>
+        Ranges: (Tokens.InlineRange * Ast.Var) list
+    }
+let varHighlightsEmpty =
+    {
+        Ma = Map.empty
+        Ranges = []
+    }
+type LocHighlights =
+    {
+        Ma: Map<Ast.LocationName, (Tokens.InlineRange * VarHighlightKind) list>
+        Ranges: (Tokens.InlineRange * Ast.LocationName) list
+    }
+let locHighlightsEmpty =
+    {
+        Ma = Map.empty
+        Ranges = []
+    }
+type Highlights =
+    {
+        VarHighlights: VarHighlights
+        LocHighlights: LocHighlights
+    }
+let highlightsEmpty =
+    {
+        VarHighlights = varHighlightsEmpty
+        LocHighlights = locHighlightsEmpty
+    }
+type 'a Parser = Parser<'a, State>
+
+and State =
+    {
+        Tokens: Tokens.Token list
+        /// Здесь ошибки только те, что могут определиться во время поверхностного семантического разбора, то есть это то, что не нуждается в нескольких проходах. Например, можно определить, что в коде пытаются переопределить встроенную функцию, и это будет ошибкой.
+        ///
+        /// А если хочется понять, что инструкция `gt 'some loc'` верна, то придется пройтись дважды, чтобы определить, существует ли вообще `'some loc'`. Если бы локации определялись последовательно, то есть нельзя было бы обратиться к той, что — ниже, тогда потребовался только один проход. Но в таком случае придется вводить что-то вроде `rec`, чтобы перейти на локацию, определенную ниже. Но всё это возвращает к той же задаче, потому ну его.
+        SemanticErrors: (Tokens.InlineRange * string) list
+        /// Информация обо всём и вся
+        Hovers: (Tokens.InlineRange * string) list
+        Highlights: Highlights
+        /// Нужен для `elseif` конструкции. Эх, если бы ее можно было как-то именно там оставить, но увы.
+        IsEndOptional : bool
+        LastSymbolPos : FParsec.Position
+        /// Локации, которые неопределенны именно в этом документе, но переходы к ним есть
+        NotDefinedLocs: Map<Ast.LocationName, Tokens.InlineRange list>
+        // Я тут, это самое, оставлю. Никто не возражает?
+        PStmts: Parser<Ast.Statement list>
+        /// `&lt;a gt ''x''>`
+        SingleQuotNestedCount: int
+        DoubleQuotNestedCount: int
+        HtmlAttDoubleNested: int
+    }
+let emptyState =
+    {
+        Tokens = []
+        SemanticErrors = []
+        Hovers = []
+        IsEndOptional = false
+        LastSymbolPos = FParsec.Position("", 0L, 1L, 1L)
+        Highlights = highlightsEmpty
+        NotDefinedLocs = Map.empty
+        PStmts = FParsec.Primitives.failFatally "PStmts not implemented"
+        SingleQuotNestedCount = 0
+        DoubleQuotNestedCount = 0
+        HtmlAttDoubleNested = 0
+    }
+
+let pGetDefLocPos locName =
+    getUserState
+    |>> fun st ->
+        match Map.tryFind locName st.Highlights.LocHighlights.Ma with
+        | None ->
+            None
+        | Some(value) ->
+            value
+            |> List.tryPick (fun (r, kind) ->
+                match kind with
+                | WriteAccess -> Some r
+                | _ -> None
+            )
+
+let appendVarHighlight (r:Tokens.InlineRange) (var:Ast.Var) highlightKind =
+    let var = mapSnd String.toLower var // for case-insensitively
+    updateUserState (fun st ->
+        { st with
+            Highlights =
+                {
+                    st.Highlights with
+                        VarHighlights =
+                            {
+                                Ranges = (r, var)::st.Highlights.VarHighlights.Ranges
+                                Ma =
+                                    let v = r, highlightKind
+                                    st.Highlights.VarHighlights.Ma
+                                    |> Map.addOrMod var [v] (fun xs -> v::xs)
+                            }
+                }
+        }
+    )
+let appendLocHighlight (r:Tokens.InlineRange) (loc:string) highlightKind =
+    let loc = String.toLower loc // без шуток, они тоже case-insensitively, хотя и представляют из себя string
+    updateUserState (fun st ->
+        { st with
+            Highlights =
+                {
+                    st.Highlights with
+                        LocHighlights =
+                            {
+                                Ranges = (r, loc)::st.Highlights.LocHighlights.Ranges
+                                Ma =
+                                    let v = r, highlightKind
+                                    st.Highlights.LocHighlights.Ma
+                                    |> Map.addOrMod loc [v] (fun xs -> v::xs)
+                            }
+                }
+        }
+    )
+
+let appendToken2 tokenType r =
+    updateUserState (fun st ->
+        let token =
+            { Tokens.TokenType = tokenType
+              Tokens.Range = r }
+        { st with Tokens = token :: st.Tokens }
+    )
+
+let toRange (p1:FParsec.Position) (p2:FParsec.Position) =
+    {
+        Tokens.InlineRange.Line = p1.Line // Должно выполняться условие `p1.Line = p2.Line`
+        Tokens.InlineRange.Column1 = p1.Column
+        Tokens.InlineRange.Column2 = p2.Column // Должно выполняться условие `p2.Column > p1.Column`
+    }
+let appendToken tokenType p =
+    getPosition .>>.? p .>>. getPosition
+    >>= fun ((p1, p), p2) ->
+        let r = toRange p1 p2
+        appendToken2 tokenType r
+        >>. preturn p
+
+let applyRange p =
+    getPosition .>>.? p .>>. getPosition
+    >>= fun ((p1, p), p2) ->
+        let range = toRange p1 p2
+        preturn (range, p)
+
+let appendHover2 msg range =
+    updateUserState (fun st ->
+        { st with Hovers = (range, msg) :: st.Hovers }
+    )
+
+let appendSemanticError range msg =
+    updateUserState (fun st ->
+        { st with SemanticErrors =
+                    (range, msg) :: st.SemanticErrors })
+
+let appendHover msg p =
+    (getPosition .>>.? p .>>. getPosition)
+    >>= fun ((p1, p), p2) ->
+        let r = toRange p1 p2
+        appendHover2 msg r
+        >>. preturn p
+let appendTokenHover tokenType msg p =
+    (getPosition .>>.? p .>>. getPosition)
+    >>= fun ((p1, p), p2) ->
+        let r = toRange p1 p2
+        appendToken2 tokenType r
+        >>. appendHover2 msg r
+        >>. preturn p
+
+let pSingleNested =
+    updateUserState (fun st ->
+        { st with
+            SingleQuotNestedCount = st.SingleQuotNestedCount + 1
+        })
+let pSingleUnnested =
+    updateUserState (fun st ->
+        { st with
+            SingleQuotNestedCount = st.SingleQuotNestedCount - 1
+        })
+let pGetSingleNested =
+    getUserState |>> fun x -> x.SingleQuotNestedCount
+let pDoubleNested =
+    updateUserState (fun st ->
+        { st with
+            DoubleQuotNestedCount = st.DoubleQuotNestedCount + 1
+        })
+let pDoubleUnnested =
+    updateUserState (fun st ->
+        { st with
+            DoubleQuotNestedCount = st.DoubleQuotNestedCount - 1
+        })
+let pGetDoubleNested =
+    getUserState |>> fun x -> x.DoubleQuotNestedCount
+let pHtmlAttDoubleNested =
+    updateUserState (fun st ->
+        { st with
+            HtmlAttDoubleNested = st.HtmlAttDoubleNested + 1
+        })
+let pHtmlAttDoubleUnnested =
+    updateUserState (fun st ->
+        { st with
+            HtmlAttDoubleNested = st.HtmlAttDoubleNested - 1
+        })
+let pGetHtmlAttDoubleNested =
+    getUserState |>> fun x -> x.HtmlAttDoubleNested
+
+open Tokens
+
+let charsReplicate n (c:char) =
+    System.String.Concat (Array.replicate n c)
+
+// Это такой фокус, чтобы напрочь во всем запутаться. А кто говорил, что это чисто функциональное программирование? Ну-ну.
+let pstmts : _ Parser =
+    getUserState >>= fun st -> st.PStmts
+let stringLiteralWithToken pexpr : _ Parser =
+    let bet tokenType openedChar closedChar pnested punnested pgetNested =
+        let p nestedCount =
+            many1Satisfy (fun c' -> not (c' = closedChar || c' = '\n' || c' = '<'))
+            <|> (pstring (charsReplicate (pown 2 nestedCount) closedChar) // 1 2, 2 4
+                 >>% string closedChar)
+            <|> (skipChar '<' >>? notFollowedBy (skipChar '<' <|> skipChar 'a' <|> skipString "/a>") >>% "<")
+        let pattValBody nestedCount closedCharAtt =
+            many1Satisfy (fun c' -> not (c' = closedChar || c' = '\n' || c' = '&' || c' = closedCharAtt))
+            <|> (pstring (charsReplicate (pown 2 nestedCount) closedChar)
+                 >>% string closedChar)
+            <|> (pchar '&'
+                 >>. ((pstring "quot" >>% "\"" <|> pstring "apos" >>% "'") .>> pchar ';'
+                      <|>% "&") )
+            // <|> (skipChar '<' >>? notFollowedBy (skipChar '<' <|> skipChar 'a' <|> skipString "/a>") >>% "<")
+        let plineKind nestedCount =
+            let plineKind, plineKindRef = createParserForwardedToRef()
+            let plineKinds =
+                pipe2
+                    (many plineKind)
+                    (many
+                        (newline >>. many plineKind))
+                    (fun x xs -> x::xs)
+            let pATag =
+                // А вот здесь вообще начинается прелюбопытная штука:
+                // 1. Все `"` экранируются в `&quot;`, а сам `&` — в `&amp;`
+                // 2. Если нужно еще вложить, то используется `&quot;&quot;`
+                pstring "<a href=\"exec:"
+                >>. (attempt // TODO: Если в значении аттрибута нету подстановки, тогда нужно пытататься разобрать его статически. К черту производительность, главное, понятность
+                       (pHtmlAttDoubleNested
+                        >>. spaces >>. notEmpty pstmts
+                        .>> pHtmlAttDoubleUnnested
+                        |>> Ast.StaticStmts)
+                     <|> (appendToken tokenType (many1Strings (pattValBody nestedCount '"')) // TODO: здесь можно и нужно отобразить подстановки.
+                          |>> Ast.Raw))
+                .>> pchar '"' .>> spaces .>> pchar '>' // что ж, не всё так просто. Дело в том, что во вложенном `pstmts` все `stringLiteral` заместо привычных `"` и `'` использует либо `&quot;` и `''`, либо `&apos;`. Да еще и `&` экранирует в `&amp;`. И всё это кучу раз вкладывается и перевкладывается. Честно сказать, голова пухнет от всех этих страстей. А еще на `if` жаловался, ну-ну.
+                .>>. plineKinds .>> pstring "</a>" // вот надо были тебе эти дурацкие вложения? Еще скажи, что хотел полноценный HTML-parser сделать. Ой, точно, хочет! Ха-ха.
+                |>> fun (stmts, line) -> Ast.HyperLinkKind(stmts, line) // Вот смотрю я на эти былины и диву даюсь, право слово. Это ж надо было до такого додуматься. Метаметамета...программирование какое-то
+            plineKindRef :=
+                appendToken tokenType (many1Strings (p nestedCount)) |>> Ast.StringKind
+                <|> (appendToken TokenType.InterpolationBegin (pstring "<<")
+                     >>. (ws >>. pexpr |>> Ast.ExprKind) // это может *немного* запутать, но, эм, но есть какое-то "но", да... Никакого "но" нету — код безнадежно запутанный 😭. Так, здесь экранизация — внутри экранизации, поэтому порождает в два раза больше открывающих скобок. Я сделал всего два уровня и наивно надеюсь, что этого хватит. То есть сейчас он обрабатывает вот эту зверюгу: `'<<''<<''''x''''>>''>>'`. Страшно, правда? Но что-то мне подсказывает, что это так не работает. Проверил, работает, что еще больше ужасает. И `'<<''<<''''<<''''''''это чудовище''''''''>>''''>>''>>'` работает...
+                     .>> ws .>> appendToken TokenType.InterpolationEnd (pstring ">>"))
+                <|> attempt pATag // TODO: тут бы предупреждение какое-нибудь не помешало: мол, не осилил
+            plineKind <|> (pchar '<' >>% Ast.StringKind "<")
+        pgetNested >>=? fun nestedCount ->
+        let pOpened = pstring (charsReplicate (pown 2 nestedCount) openedChar)
+        let pClosed = pstring (charsReplicate (pown 2 nestedCount) closedChar)
+        let plineKind = plineKind (nestedCount + 1)
+
+        appendToken tokenType (pOpened .>> pnested)
+        >>. pipe2
+                (many plineKind)
+                (many
+                    (newline >>. many plineKind)
+                 .>> punnested
+                 .>> appendToken tokenType pClosed) // TODO: Здесь самое то использовать `PunctuationDefinitionStringEnd`
+                (fun x xs -> (x:Ast.Line)::xs)
+    bet TokenType.StringQuotedSingle '\'' '\'' pSingleNested pSingleUnnested pGetSingleNested
+    <|> (pGetHtmlAttDoubleNested >>=? fun x ->
+         if x > 0 then
+            fail "not implemented HtmlAttDoubleNested"
+         else
+            bet TokenType.StringQuotedDouble '"' '"' pDoubleNested pDoubleUnnested pGetDoubleNested)
+
+let pbraces tokenType : _ Parser =
+    let pbraces, pbracesRef = createParserForwardedToRef()
+    let p = many1Satisfy (isNoneOf "{}\n")
+
+    pbracesRef :=
+        pipe2
+            (appendToken tokenType
+                (many1Satisfy2 ((=) '{') (isNoneOf "{}\n")) )
+            (many
+                (appendToken tokenType (many1Strings p)
+                 <|> newlineReturn "\n"
+                 <|> pbraces
+                )
+             .>>. appendToken tokenType (pchar '}'))
+            (fun x (xs, closedChar) ->
+                seq {
+                    yield x
+                    yield! xs
+                    yield string closedChar
+                }
+                |> System.String.Concat
+            )
+    pipe2
+        (appendToken tokenType
+            (pchar '{' >>. manyStrings p)
+         .>>. opt (newlineReturn "\n"))
+        (many
+            (appendToken tokenType (many1Strings p)
+             <|> newlineReturn "\n"
+             <|> pbraces
+            )
+         .>> appendToken tokenType (pchar '}')) // TODO: Здесь самое то использовать `PunctuationDefinitionStringEnd`
+        (fun (x, nl) xs ->
+            match nl with
+            | None ->
+                x::xs |> System.String.Concat
+            | Some nl ->
+                x::nl::xs |> System.String.Concat)

+ 25 - 0
QSParse/QSParse.fsproj

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <OutputType>Library</OutputType>
+    <TargetFramework>net461</TargetFramework>
+    <Optimize>true</Optimize>
+    <Tailcalls>true</Tailcalls>
+  </PropertyGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+    <Compile Include="Tokens.fs" />
+    <Compile Include="Ast.fs" />
+    <Compile Include="Defines.fs" />
+    <Compile Include="Show.fs" />
+    <Compile Include="ParserGeneric.fs" />
+    <Compile Include="ParserExpr.fs" />
+    <Compile Include="Parsec.fs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+    <Reference Include="System.Runtime" />
+    <Reference Include="..\paket-files\github.com\gretmn102\FsharpMyExtension\FsharpMyExtension\FsharpMyExtension\bin\Debug\net461\FsharpMyExtension.dll" />
+  </ItemGroup>
+  <Import Project="..\.paket\Paket.Restore.targets" />
+</Project>

+ 323 - 0
QSParse/Show.fs

@@ -0,0 +1,323 @@
+module Qsp.Show
+open FsharpMyExtension
+open FsharpMyExtension.ShowList
+open Qsp.Ast
+
+type FormatConfig =
+    {
+        IsSplitStringPl: bool
+        TrimWhitespaceWhenSplit: bool
+    }
+    static member Default =
+        {
+            IsSplitStringPl = false
+            TrimWhitespaceWhenSplit = false
+        }
+
+let showVarType = function
+    | StringType -> showChar '$'
+    | ImplicitNumericType -> empty
+    | ExplicitNumericType -> showChar '#'
+let showVar (typ:VarType, varName:string) =
+    showVarType typ << showString varName
+
+let rec showStringLines showExpr showStmtsInline (lines:list<Line>) =
+    lines
+    |> List.map (
+        List.collect (
+            function
+            | StringKind x ->
+                showString (x.Replace("'", "''"))
+                |> List.singleton
+            | ExprKind x ->
+                showExpr x
+                |> show
+                |> fun x -> x.Replace("'", "''") // TODO: стоит ли говорить, что все эти былины с `.Replace("'", "''")` нужно превратить в нормальный код?
+                |> showString
+                |> bet "<<" ">>"
+                |> List.singleton
+            | HyperLinkKind(x, body) ->
+                let attValue =
+                    match x with
+                    | Raw x ->
+                        x.Replace("'", "''")
+                        |> showString
+                    | StaticStmts(x) ->
+                        showStmtsInline x
+                        |> show
+                        |> fun x -> x.Replace("'", "''")
+                        |> showString
+                let header =
+                    showString "<a href=\"exec: "
+                    << attValue
+                    << showString "\">"
+                match showStringLines showExpr showStmtsInline body with
+                | [] ->
+                    header
+                    << showString "</a>"
+                    |> List.singleton
+                | [x] ->
+                    header
+                    << x
+                    << showString "</a>"
+                    |> List.singleton
+                | xs ->
+                    xs
+                    |> List.mapStartMidEnd
+                        (fun x -> header << x)
+                        id
+                        (fun x -> x << showString "</a>")
+                    |> fun x -> x // TODO: и все строки позже соединятся воедино, даже пробелов не удостоятся, ага.
+        ) >> joinsEmpty empty
+    )
+let showValue showExpr showStmtsInline = function
+    | Int x -> shows x
+    | String lines ->
+        showStringLines showExpr showStmtsInline lines
+        |> joinsEmpty (showString "\n")
+        |> bet "'" "'"
+let ops = Op.toString >> showString
+
+let unar = function No -> "no" | Obj -> "obj" | Neg -> "-" | Loc -> "loc"
+
+let rec simpleShowExpr showStmtsInline expr : ShowS =
+    let rec f = function
+        | Val v -> showValue (simpleShowExpr showStmtsInline) showStmtsInline v
+        | Var v -> showVar v
+        | Func(name, args) ->
+            let args =
+                if List.isEmpty args then
+                    empty
+                else
+                    showParen true (List.map f args |> join ", ")
+            showString name << args
+        | UnarExpr(op, e) ->
+            let space = function Obj | No | Loc -> showSpace | Neg -> id
+            let x =
+                match e with
+                | Expr(_, _, _) ->
+                    showParen true (f e)
+                | Arr(_, _) // `-(arr[idx])` лучше выглядит, чем `-arr[idx]`?
+                | Func(_, _) // `-(func(idx))` лучше выглядит, чем `-(arr(idx))`?
+                | UnarExpr _
+                | Val _
+                | Var _ ->
+                    space op << f e
+            showString (unar op) << x
+        | Expr(op, e1, e2) ->
+            let f body =
+                match body with
+                | Val(_)
+                | Var _ ->  f body
+                | UnarExpr(_, _)
+                | Expr(_, _, _) ->
+                    showParen true (f body)
+                | Func(_, _)
+                | Arr(_, _) ->
+                    f body
+            f e1 << showSpace
+            << ops op << showSpace
+            << f e2
+        | Arr(var, es) ->
+            showVar var << bet "[" "]" (List.map f es |> join ", ")
+    f expr
+let rec showExpr showStmtsInline = function
+    | Val v -> showValue (showExpr showStmtsInline) showStmtsInline v
+    | Var v -> showVar v
+    | Func(name, args) ->
+        let args =
+            if List.isEmpty args then
+                empty
+            else
+                showParen true
+                    (List.map (showExpr showStmtsInline) args |> join ", ")
+        showString name << args
+    | UnarExpr(op, e) ->
+        let space = function Obj | No | Loc -> showSpace | Neg -> id
+        showString (unar op) << space op << showExpr showStmtsInline e
+    | Expr(op, e1, e2) ->
+        let prec = Precedences.OpB >> Precedences.prec
+        let f = function
+            | Expr(op', _, _) -> showParen (prec op > prec op')
+            | UnarExpr _ -> showParen true | _ -> id
+        let f x = f x (showExpr showStmtsInline x)
+        f e1 << showSpace << ops op << showSpace << f e2
+    | Arr(var, es) -> showVar var << bet "[" "]" (List.map (showExpr showStmtsInline) es |> join ", ")
+
+
+let showAssign showStmtsInline = function
+    | AssignWhat.AssignArr(var, key) -> showVar var << bet "[" "]" (showExpr showStmtsInline key)
+    | AssignWhat.AssignVar var -> showVar var
+    | AssignWhat.AssignArrAppend var -> showVar var << showString "[]"
+
+let (|OneStmt|_|) = function
+    | [x] ->
+        match x with
+        // | StarPl(Val (String _)) -> None
+        | StarPl _ -> None // Как правило, строки очень длинные, потому пусть лучше будет так
+        | Assign _ | CallSt _ | Comment _ -> Some x
+        | AssignCode _ -> None // спорно
+        | Act _ | If _ -> None
+        | Label _ -> None // эту нечисть нужно как можно более нагляднее подчеркнуть. Да странно будет, если она окажется одна в списке инструкций.
+        | Exit -> None // ¯\_(ツ)_/¯
+        | For _ -> None
+    | _ -> None
+
+let (|AssingName|) = function AssignArr(x, _) -> x | AssignVar x -> x | AssignArrAppend x -> x
+type IndentsOption =
+    | UsingSpaces of int
+    | UsingTabs
+
+let spaceBetween (s:ShowS) : ShowS =
+    showSpace << s << showSpace
+let showStmt indentsOption (formatConfig:FormatConfig) =
+    let tabs =
+        match indentsOption with
+        | UsingTabs ->
+            showChar '\t'
+        | UsingSpaces spacesCount ->
+            replicate spacesCount ' '
+    let rec f' stmt =
+        let showStmtsInline xs : ShowS =
+            List.collect f' xs // TODO
+            |> join "&"
+        let showAssign = showAssign showStmtsInline
+        let showExpr = showExpr showStmtsInline
+        let showStringLines = showStringLines showExpr showStmtsInline
+        match stmt with
+        | Assign(AssingName name' as ass, Expr((Plus|Minus) as op, Var name, e)) when name' = name ->
+            [showAssign ass << spaceBetween (ops op << showChar '=') << showExpr e]
+        | Assign(AssingName name' as ass, Expr((Plus|Minus) as op, e, Var name)) when name' = name ->
+            [showAssign ass << spaceBetween (showChar '=' << ops op) << showExpr e]
+        | Assign(ass, e) ->
+            [showAssign ass << spaceBetween (showChar '=') << showExpr e]
+        | CallSt(name, es) ->
+            let args =
+                if List.isEmpty es then
+                    empty
+                else
+                    showSpace << (List.map showExpr es |> join ", ")
+            [ showString name << args ]
+        | StarPl e ->
+            if formatConfig.IsSplitStringPl then
+                match e with
+                | Val(String str) ->
+                    let str =
+                        if formatConfig.TrimWhitespaceWhenSplit then
+                            str
+                            |> List.map (
+                                List.map (function
+                                    | StringKind x -> StringKind (x.Trim())
+                                    | x -> x)
+                            )
+                        else
+                            str
+                    showStringLines str
+                    |> List.map (bet "'" "'")
+                | _ ->
+                    [ showExpr e ]
+            else
+                [ showExpr e ]
+        | Label s -> [showChar ':' << showString s]
+        | If(e, thenBody, elseBody) ->
+            let ifHeader e = showString "if" << showSpace << showExpr e << showChar ':'
+            [
+                match thenBody, elseBody with
+                | OneStmt x, OneStmt y ->
+                    yield ifHeader e
+                          << showSpace << showStmtsInline [x]
+                          << spaceBetween (showString "else")
+                          << showStmtsInline [y]
+                | OneStmt x, [] ->
+                    yield ifHeader e
+                          << showSpace << showStmtsInline [x]
+                | _ ->
+                    let rec body : _ -> ShowS list = function
+                        | [If(e, thenBody, elseBody)] ->
+                            [
+                                yield showString "elseif" << showSpace << showExpr e << showChar ':'
+                                yield! thenBody
+                                       |> List.collect
+                                           (f' >> List.map ((<<) tabs))
+                                yield! body elseBody
+                            ]
+                        | [] -> []
+                        | xs ->
+                            [
+                                yield showString "else"
+                                yield!
+                                    xs
+                                    |> List.collect
+                                        (f' >> List.map ((<<) tabs))
+                            ]
+                    yield ifHeader e
+                    yield! thenBody
+                           |> List.collect
+                               (f' >> List.map ((<<) tabs))
+                    yield! body elseBody
+                    yield showString "end"
+            ]
+        | Act(es, body) ->
+            let header = showString "act" << showSpace << join ", " (List.map showExpr es) << showChar ':'
+            [
+                match body with
+                | OneStmt x ->
+                    yield header << showSpace << showStmtsInline [x]
+                | _ ->
+                    yield header
+                    yield!
+                        body
+                        |> List.collect
+                            (f' >> List.map ((<<) tabs))
+                    yield showString "end"
+            ]
+        | For(var, fromExpr, toExpr, body) ->
+            let header =
+                showString "for"
+                << showSpace << showVar var
+                << showSpace << showChar '='
+                << showSpace << showExpr fromExpr
+                << showSpace << showString "to"
+                << showSpace << showExpr toExpr
+                << showChar ':'
+            [
+                match body with
+                | OneStmt x ->
+                    yield header << showSpace << showStmtsInline [x]
+                | _ ->
+                    yield header
+                    yield!
+                        body
+                        |> List.collect
+                            (f' >> List.map ((<<) tabs))
+                    yield showString "end"
+            ]
+        | Comment s -> [showChar '!' << showString s]
+        | AssignCode(ass, stmts) ->
+            let header = showAssign ass << spaceBetween (showChar '=') << showChar '{'
+            [
+                if List.isEmpty stmts then
+                    yield header << showChar '}'
+                else
+                    yield header
+                    yield!
+                        stmts
+                        |> List.collect
+                            (f' >> List.map ((<<) tabs))
+                    yield showChar '}'
+            ]
+
+        | Exit -> [showString "exit"]
+    f'
+
+let showLoc indentsOption isSplitStringPl (Location(name, statements)) : ShowS list =
+    [
+        yield showString "# " << showString name
+        yield! List.collect (showStmt indentsOption isSplitStringPl) statements
+        yield showString (sprintf "--- %s ----------" name)
+    ]
+
+let printLocs indentsOption isSplitStringPl xs =
+    List.map (lines << showLoc indentsOption isSplitStringPl) xs
+    |> joinEmpty "\n\n"
+    |> show

+ 67 - 0
QSParse/Tokens.fs

@@ -0,0 +1,67 @@
+module Qsp.Tokens
+
+type Range = FParsec.Position * FParsec.Position
+
+type TokenType =
+    /// в TS `var` называется `storage.type.js`
+    | Type
+    | Keyword
+    /// `act`, `if`, `:`, `end`
+    | KeywordControl
+    | Function
+    /// В QSP `comment.line` и `comment.block` объединены
+    | Comment
+
+    | Procedure
+    | Variable
+    /// `keyword.operator.assignment.js`
+    ///
+    /// `=`
+    | OperatorAssignment
+    /// `keyword.operator.arithmetic.js`
+    ///
+    /// `-` `+` `*` `/`
+    | OperatorArithmetic
+    /// `keyword.operator.comparison.js`
+    ///
+    /// `=`
+    | OperatorComparison
+    /// `keyword.operator.relational.js`
+    ///
+    /// `>` `>=` `<` `<=`
+    | OperatorRelational
+    /// `punctuation.terminator.statement.js`
+    ///
+    /// `&`
+    | PunctuationTerminatorStatement
+
+    // | PunctuationDefinitionStringBegin
+    // | PunctuationDefinitionStringEnd
+    | StringQuotedDouble
+    | StringQuotedSingle
+    | StringBraced
+    // | ConstantCharacterEscape
+    /// `entity.name.label.cs`
+    | NameLabel
+
+    /// `punctuation.separator.colon.cs`
+    | LabelColon
+    /// `punctuation.definition.interpolation.begin.cs`
+    ///
+    /// `<<`
+    | InterpolationBegin
+    /// `punctuation.definition.interpolation.end.cs`
+    ///
+    /// `>>`
+    | InterpolationEnd
+
+    | ConstantNumericInteger
+type InlineRange =
+    {
+        Line: int64
+        Column1: int64
+        Column2: int64
+    }
+type Token =
+    { TokenType: TokenType
+      Range: InlineRange }

+ 3 - 0
QSParse/paket.references

@@ -0,0 +1,3 @@
+FSharp.Core
+FuChu
+FParsec

+ 6 - 2
QspServer/QspServer.fsproj

@@ -6,6 +6,11 @@
     <Optimize>true</Optimize>
     <Tailcalls>true</Tailcalls>
   </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\QSParse\QSParse.fsproj">
+      <Name>QSParse.fsproj</Name>
+    </ProjectReference>
+  </ItemGroup>
   <ItemGroup>
     <Compile Include="..\paket-files\fsharp\FsAutoComplete\src\LanguageServerProtocol\LanguageServerProtocol.fs">
       <Paket>True</Paket>
@@ -14,8 +19,7 @@
     <Compile Include="Program.fs" />
   </ItemGroup>
   <ItemGroup>
-    <Reference Include="..\paket-files\github.com\gretmn102\FParserQSP\paket-files\github.com\gretmn102\FsharpMyExtension\FsharpMyExtension\FsharpMyExtension\bin\Release\net461\FsharpMyExtension.dll" />
-    <Reference Include="..\paket-files\github.com\gretmn102\FParserQSP\QSParse\bin\Debug\net461\QSParse.dll" />    
+    <Reference Include="..\paket-files\github.com\gretmn102\FsharpMyExtension\FsharpMyExtension\FsharpMyExtension\bin\Debug\net461\FsharpMyExtension.dll" />
   </ItemGroup>
   <Import Project="..\.paket\Paket.Restore.targets" />
 </Project>

+ 1 - 1
README.md

@@ -1 +1 @@
-QSP LSP for VS Code extension.
+[QSP](http://wiki.qsp.su/) LSP for VS Code extension.

+ 61 - 0
TODO

@@ -0,0 +1,61 @@
+Синтаксис:
+    ✔ Однострочный `else` @done(20-07-06 20:37)
+    ☐ `_` в выражениях
+    ☐ `if x = 10: ! comment`
+    ☐ Вызов процедуры `The(Lady), 'or', the, Tiger`. Сейчас такое `*p(Lady), 'or', the, Tiger` работает
+    ✔ Допускать на уровне синтаксиса неопределенные процедуры со звездочкой (например, `*proc`), а уже на уровне семантики запрещать @done
+    ✔ Лишний `end` @done(20-07-13 01:22)
+    ✔ Бардак с комментариями @done(20-07-10 08:09)
+    ☐ Разобраться, почему parser всё еще допускает `proc call someAnotherStatement`, хотя изначально нужен `&`: `proc call & someAnotherStatement`. Явно что-то упустил, да и код станет понятнее, если инструкции как-то явно разбивать.
+    ✔ В строках есть подстановка (например, `'<<expr>>'`) @done(20-07-09 23:00)
+    ✔ Строковые значения в подстановке `'<<''str''>>'` и `"<<""x"">>"`, соответственно. Эх, ну зачем так делать? Можно же без экранизация обойтись, что ж вы за люди такие? Можно же так `'<<'str'>>'` распарсить. Можно было бы сказать, что мы просто хотим использовать воспользоваться выражением `'<<' + 'str'`, так и здесь выбьет ошибку: мол, непарное количество скобок. Ладно, ничего не поделаешь — придется делать.
+    ☐ Полноценный HTML-parser
+    ✔ Эх, а еще есть `<a href="exec:`. В этом мире слишком много зла. Хотя бы эту штуку придется сделать, если хочется полностью схватить все вызовы локаций.
+    ☐ Если конструкция `if`/`act` обрывается на завершении локации, то можно так и сказать, а не расписывать полотно с ожидаемыми инструкциями
+    ☐ Лишние `)`
+    ✔ `$a[] = 'x'` — вполне допустимая конструкция, которая добавляет в конце массива элемент с таким-то значением. Ничего особенно, правда? Язык прост и предельно понятен, да... :face_palm: @done(20-07-14 02:13)
+    ✔ `for` точно существует в Quest Navigator'е и точно не работает в QSP 5.7.0 под Windows
+    ☐ `for нечётные = 1 to 10 step 2: *pl нечётные`
+    ☐ Как-то нужно определять, что конструкция `for` разрешена или нет
+    ☐ `gs 'loc', , 1` — пропуск аргумента. В QSP 5.7.0 под Windows не работает, но работает в Quest Navigator, кажется.
+Подсказки:
+    ✔ Ключевые слова
+    ✔ Функции
+    ✔ Процедуры
+    ✔ @low Бинарные операторы
+Семантика:
+    ☐ `local v = 1` — жуть, правда? В Quest Navigator'е есть, а в QSP 5.7.0 под Windows нету. Синтаксически проходит как процедура `local` с параметром `v = 1`, так что дело за семантикой
+    ☐ А ведь переменные `args` и `$curloc` для каждой локации локальны, значит, пришло время строить что-то более серьезное с областью видимости переменных.
+    ✔ Вызов несуществующей функции/процедуры
+    ✔ Переопределение процедур/функций
+    ✔ Количество аргументов к процедурам @done(20-07-09 01:09)
+    ✔ Количество аргументов к функциям @done(20-07-09 11:11)
+    Явные ошибки присваивания:
+        ☐ `a = 'foo'`, `#a = 'foo'`, `a[expr] = 'foo'`, `#a[expr] = 'foo'`
+    ✔ Признавать `cla` и прочие процедуры без аргументов как процедуры, а не переменные, как это происходит сейчас @done(20-07-09 13:58)
+    ✔ `add obj $someobj` — что делать с этим безобразием? Синтаксически это — вызов процедуры `add` с выражением `obj $someobj`, которое возвращает 0, если предмета `$someobj` нет в инвентаре, как заявлено в документации префиксного оператора `obj`. Однако документация говорит, что выражение `add obj [$выражение]` воспринимается как `addobj [$выражение]`. Какого лешего? Это контринтуитивно и глупо. `close all` — туда же вместе с `del act` и `del obj`.
+    ✔ `MSG('content')` @done(20-07-09 13:58)
+Статический анализ:
+    ☐ Удаление предмета, который нигде не добавляется
+    ☐ Переменная присваивается, но ни разу не используется (заданные переменные — исключение)
+    ☐ Переменная используется, но ни разу не присваивается (заданные переменные — исключение)
+    ☐ Инструкции после `exit`, `jump` и прочих операторов перехода не сработают
+Highlight:
+    ✔ Комментарий все еще не разбивает токены на несколько `Range` @done(20-07-06 11:29)
+    ✔ Однотонный цвет комментария @done
+    ✔ @low бинарные операторы
+    ✔ Variable highlight @done(20-07-09 20:22)
+    ✔ Label highlight
+    ☐ Jump label highlight
+    ☐ Label highlight in `func('label')` or `$func('label')`
+Форматирование:
+    ☐ `stmt & ! comment`
+    ✔ Убрать пробел после вызова процедуры без аргументов, например, `*pl ` @done(20-07-09 14:08)
+    ✔ Разбивать строки, если они разделены `\n` (сделать выборочно)
+    ✔ `$str = {}` превращает в: @done(20-07-10 10:26)
+      ```qsp
+      $str = {
+
+      }
+      ```
+☐ Работа с проектом, то есть не просто с одним файлом, а с целым множеством.

+ 32 - 0
Test/OldAndNewExprTests.fs

@@ -0,0 +1,32 @@
+module OldAndNewExprTests
+open Fuchu
+
+let testf input =
+    testCase input <| fun _ ->
+        let f input =
+            let exp =
+                Qsp.Parser.Generic.runStateEither Qsp.Parser.Expr.pExprOld Qsp.Parser.Generic.emptyState input
+                |> snd
+            let act =
+                Qsp.Parser.Generic.runStateEither Qsp.Parser.Expr.pExprNew Qsp.Parser.Generic.emptyState input
+                |> snd
+            exp, act
+        let exp, act = f input
+        Assert.Equal("", exp, act)
+[<Tests>]
+let tests =
+    testList "expr test" [
+        testf "1 + 2 * 3"
+        testf "1 + 2 mod 3 * 4"
+        testf "-1 - -1"
+        testf "-(a + b)"
+        testf "var1 and var2 and no var3 and obj var4"
+        testf "var1[var1 + var2] and func(arg1, arg2[expr], x + y)"
+        testf "a = 10 or b = 20 and c = 30"
+        testf "a = pstam> (pmaxstam/4)*2 and pstam <= (pmaxstam/4)*3"
+        testf "no obj 'apple'"
+        testf "a = no -a > b"
+        testf "a and b = c and d"
+        testf "obj 'яблоко' = 0"
+    ]
+let start () = run tests

+ 861 - 31
Test/Test.fs

@@ -1,36 +1,866 @@
-open Fuchu
 open FsharpMyExtension
+open FsharpMyExtension
+open FsharpMyExtension.Either
+open FParsec
+#if INTERACTIVE
+#load @"..\QSParse\Tokens.fs"
+#load @"..\QSParse\Ast.fs"
+#load @"..\QSParse\Defines.fs"
+#load @"..\QSParse\Show.fs"
+#load @"..\QSParse\ParserGeneric.fs"
+#load @"..\QSParse\ParserExpr.fs"
+#load @"..\QSParse\Parsec.fs"
+#endif
+open Qsp
+open Qsp.Ast
+open Qsp.Parser.Generic
+open Qsp.Parser.Expr
+
+
+open Fuchu
+[<Tests>]
+let pexprTest =
+    let runExpr str =
+        runStateEither pexpr Qsp.Parser.Generic.emptyState str
+        |> snd
+    let sprintExpr =
+        Show.simpleShowExpr (failwithf "showStmtsInline not implemented %A")
+        >> FsharpMyExtension.ShowList.show
+    let runExprShow str =
+        runExpr str
+        |> Either.map sprintExpr
+    let equalWithShow (exp:Expr) (act:Either<_, Expr>) =
+        match act with
+        | Left _ ->
+            failtestf "%A" act
+        | Right act ->
+            if exp <> act then
+                failtestf "Expected:\n%A\n\"%s\"\n\nActual:\n%A\n\"%s\"" exp (sprintExpr exp) act (sprintExpr act)
+    let testf input exp =
+        testCase input <| fun () ->
+            equalWithShow exp (runExpr input)
+    testList "pexpr test" [
+        testCase "строчные бинарные операторы и названия переменных, которые начинаются с них" <| fun () ->
+            let input = "notFollowedBy" // Уж точно не должно быть "no tFollowedBy"
+            let exp =
+                Var (ImplicitNumericType, "notFollowedBy")
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "строчные бинарные операторы и названия переменных, которые начинаются с них" <| fun () ->
+            let input = "object"
+            let exp =
+                Var (ImplicitNumericType, "object")
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "строчные бинарные операторы и названия переменных, которые начинаются с них" <| fun () ->
+            let input = "obj something"
+            let exp =
+                UnarExpr (Obj, Var (ImplicitNumericType, "something"))
+            Assert.Equal("", Right exp, runExpr input)
+
+        let input = "var1 and var2 and no var3 and obj var4"
+        let exp =
+            Expr
+              (And,
+               Expr
+                 (And,
+                  Expr
+                    (And, Var (ImplicitNumericType, "var1"),
+                     Var (ImplicitNumericType, "var2")),
+                  UnarExpr (No, Var (ImplicitNumericType, "var3"))),
+               UnarExpr (Obj, Var (ImplicitNumericType, "var4")))
+        testf input exp
+
+        testCase "2" <| fun () ->
+            let input = "var1[var1 + var2] and func(arg1, arg2[expr], x + y)"
+            let exp = "var1[var1 + var2] and func(arg1, arg2[expr], x + y)"
+            Assert.Equal("", Right exp, runExprShow input)
+        testCase "3" <| fun () ->
+            let input = "a = 10 or b = 20 and c = 30"
+            let exp = "(a = 10) or ((b = 20) and (c = 30))"
+            Assert.Equal("", Right exp, runExprShow input)
+        testCase "4" <| fun () ->
+            let input = "a = pstam> (pmaxstam/4)*2 and pstam <= (pmaxstam/4)*3"
+            let exp = "((a = pstam) > ((pmaxstam / 4) * 2)) and (pstam <= ((pmaxstam / 4) * 3))"
+            Assert.Equal("", Right exp, runExprShow input)
+        testCase "no obj 'apple'" <| fun () ->
+            let input = "no obj 'apple'"
+            let exp =
+                UnarExpr (No, UnarExpr (Obj, Val (String [[StringKind "apple"]])))
+            Assert.Equal("", Right exp, runExpr input)
+
+        let input = "- x"
+        let exp =
+            UnarExpr (Neg, Var (ImplicitNumericType, "x"))
+        testf input exp
+
+        let input = "-x + -y"
+        let exp =
+            Expr
+              (Plus, UnarExpr (Neg, Var (ImplicitNumericType, "x")),
+               UnarExpr (Neg, Var (ImplicitNumericType, "y")))
+        testf input exp
+    ]
+// #load "Parsec.fs"
 
 [<Tests>]
-let simpleTest =
-    testCase "simpleTest" (fun _ ->
-        let exp = "a"
-        let act = "a"
-        Assert.Equal("msg", exp, act)
-    )
-// run simpleTest // <- если нужно запустить тест вручную
-
-[<Tests>]
-let simpleTestList =
-    testList "testListName" [
-        testCase "testCase1" (fun _ ->
-            let exp = true
-            let act = true
-            Assert.Equal("msg1", exp, act)
-        )
-        testCase "testCase2" (fun _ ->
-            let exp = 1
-            let act = 1
-            Assert.Equal("msg2", exp, act)
-        )
-        testCase "testCase3" (fun _ ->
-            let exp = ()
-            let act = ()
-            Assert.Equal("msg3", exp, act)
-        )
-    ]
-// run simpleTestList
+let assignTest =
+    let runExpr str =
+        Qsp.Parser.Generic.runStateEither (Qsp.Parser.Main.pAssign FParsec.Primitives.pzero) Qsp.Parser.Generic.emptyState str
+        |> snd
+    testList "assignTest" [
+        testCase "implicit assign implicit var" <| fun () ->
+            let input = "x = 21 + 21"
+            let exp =
+                (Assign
+                   (AssignVar (ImplicitNumericType, "x"),
+                    Expr (Plus, Val (Int 21), Val (Int 21))))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "implicit assign implicit array var" <| fun () ->
+            let input = "x[expr] = 42"
+            let exp =
+                (Assign
+                   (AssignArr
+                      ((ImplicitNumericType, "x"), Var (ImplicitNumericType, "expr")),
+                    Val (Int 42)))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "implicit `-=` implicit var" <| fun () ->
+            let input = "years -= 10"
+            let exp =
+              (Assign
+                 (AssignVar (ImplicitNumericType, "years"),
+                  Expr (Minus, Var (ImplicitNumericType, "years"), Val (Int 10))))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "implicit `-=` implicit var 2" <| fun () ->
+            let input = "php -= 3*emdmg*2 - parm"
+            let exp =
+                (Assign
+                   (AssignVar (ImplicitNumericType, "php"),
+                    Expr
+                      (Minus, Var (ImplicitNumericType, "php"),
+                       Expr
+                         (Minus,
+                          Expr
+                            (Times,
+                             Expr (Times, Val (Int 3), Var (ImplicitNumericType, "emdmg")),
+                             Val (Int 2)), Var (ImplicitNumericType, "parm")))))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "5" <| fun () ->
+            let input = "a = a = no -a > b"
+            let exp =
+                (Assign
+                   (AssignVar (ImplicitNumericType, "a"),
+                    Expr
+                      (Eq, Var (ImplicitNumericType, "a"),
+                       UnarExpr
+                         (No,
+                          Expr
+                            (Gt, UnarExpr (Neg, Var (ImplicitNumericType, "a")),
+                             Var (ImplicitNumericType, "b"))))))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "implicit assign explicit array var" <| fun () ->
+            let input = "$x[expr] = 42"
+            let exp =
+                (Assign
+                   (AssignArr ((StringType, "x"), Var (ImplicitNumericType, "expr")),
+                    Val (Int 42)))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "implicit assign explicit var" <| fun () ->
+            let input = "#x = 21 + 21"
+            let exp =
+                (Assign
+                   (AssignVar (ExplicitNumericType, "x"),
+                    Expr (Plus, Val (Int 21), Val (Int 21))))
+            Assert.Equal("", Right exp, runExpr input)
+        testCase "`x[] = 1`" <| fun () ->
+            let input = "x[] = 1"
+            let exp =
+                Assign (AssignArrAppend (ImplicitNumericType, "x"), Val (Int 1))
+            Assert.Equal("", Right exp, runExpr input)
+        // ложные случаи:
+        testCase "attempt assign function" <| fun () ->
+            let input = "f(expr) = 42" // поскольку `=` — это одновременно и оператор присваивания и оператор равности, так что сойдет за выражение
+            let exp =
+                [
+                    "Error in Ln: 1 Col: 1"
+                    "f(expr) = 42"
+                    "^"
+                    "Expecting: '#', '$', 'let' (case-insensitive) or 'set' (case-insensitive)"
+                    ""
+                    "The parser backtracked after:"
+                    "  Error in Ln: 1 Col: 2"
+                    "  f(expr) = 42"
+                    "   ^"
+                    "  Expecting: '+=', '-=', '=', '=+', '=-' or '['"
+                    ""
+                ] |> String.concat "\r\n"
+            Assert.Equal("", Left exp, runExpr input)
+        testCase "attempt assign var without body" <| fun () ->
+            let input = "justName"
+            let act =
+                runExpr input
+                |> Option.ofEither
+            Assert.None("", act)
+        testCase "attempt assign var without body space" <| fun () ->
+            let input = "justName "
+            let act =
+                runExpr input
+                |> Option.ofEither
+            Assert.None("", act)
+        testCase "just `x[expr]`" <| fun () ->
+            let input = "x[expr]"
+            let act =
+                runExpr input
+                |> Option.ofEither
+            Assert.None("", act)
+    ]
+
+[<Tests>]
+let stringLiteralTest =
+    testList "stringLiteralTest" [
+        testCase "1" <| fun () ->
+            Assert.Equal("", Right " ", runEither stringLiteral "\" \"")
+        testCase "2" <| fun () ->
+            Assert.Equal("", Right "\"", runEither stringLiteral "\"\"\"\"")
+        testCase "3" <| fun () ->
+            Assert.Equal("", Right "\"'\"", runEither stringLiteral "\"\"\"'\"\"\"")
+        testCase "5" <| fun () ->
+            Assert.Equal("", Right "", runEither stringLiteral "''")
+        testCase "6" <| fun () ->
+            Assert.Equal("", Right "'", runEither stringLiteral "''''")
+        testCase "4" <| fun () ->
+            Assert.Equal("", Right "\"", runEither stringLiteral "'\"'")
+        testCase "braces1" <| fun () ->
+            Assert.Equal("", Right "abc", runEither stringLiteral "{abc}")
+        testCase "braces escaped" <| fun () ->
+            Assert.Equal("", Right "}", runEither stringLiteral "{}}}")
+    ]
+
+[<Tests>]
+let stringLiteralWithTokenTest =
+    let runEither str =
+        Qsp.Parser.Generic.runStateEither
+            (stringLiteralWithToken pexpr)
+            { Qsp.Parser.Generic.emptyState with
+                PStmts = Parser.Main.pstmts
+            }
+            str
+        |> snd
+    let f str =
+        [[StringKind str]]
+    testList "stringLiteralWithTokenTest" [
+        testCase "1" <| fun () ->
+            Assert.Equal("", Right (f " "), runEither "\" \"")
+        testCase "2" <| fun () ->
+            Assert.Equal("", Right (f "\""), runEither "\"\"\"\"")
+        testCase "3" <| fun () ->
+            Assert.Equal("", Right (f "\"'\""), runEither "\"\"\"'\"\"\"")
+        testCase "5" <| fun () ->
+            Assert.Equal("", Right [[]], runEither "''")
+        testCase "6" <| fun () ->
+            Assert.Equal("", Right (f "'"), runEither "''''")
+        testCase "4" <| fun () ->
+            Assert.Equal("", Right (f "\""), runEither "'\"'")
+        testCase "multiline `'` test" <| fun () ->
+            let input =
+                [
+                    "'"
+                    "    a"
+                    "'"
+                ] |> String.concat "\n"
+            let exp =
+                [
+                    []
+                    [ StringKind "    a"]
+                    []
+                ]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "multiline `'` test2" <| fun () ->
+            let input =
+                [
+                    "'"
+                    "    a"
+                    ""
+                    "b"
+                    "'"
+                ] |> String.concat "\n"
+            let exp =
+                [
+                    []
+                    [ StringKind "    a" ]
+                    []
+                    [ StringKind "b" ]
+                    []
+                ]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "test '<<''x''>>'" <| fun () ->
+            let input = "'<<''x''>>'"
+            let exp = [[ExprKind (Val (String [[StringKind "x"]]))]]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "test '<<''<<''''x''''>>''>>'" <| fun () ->
+            let input = "'<<''<<''''x''''>>''>>'"
+            let exp = [[ExprKind (Val (String [[ExprKind (Val (String [[StringKind "x"]]))]]))]]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "test '<<''<<''''<<''''''''x''''''''>>''''>>''>>'" <| fun () ->
+            let input = "'<<''<<''''<<''''''''x''''''''>>''''>>''>>'"
+            let exp =
+              [[ExprKind
+                  (Val
+                     (String
+                        [[ExprKind
+                            (Val (String [[ExprKind (Val (String [[StringKind "x"]]))]]))]]))]]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "test \"<<'x'>>\"" <| fun () ->
+            let input = "\"<<'x'>>\""
+            let exp = [[ExprKind (Val (String [[StringKind "x"]]))]]
+            Assert.Equal("", Right exp, runEither input)
+
+        testCase "test '<a href=\"exec:GT ''changes''\">changes</a>'" <| fun () ->
+            let input = "'<a href=\"exec:GT ''changes''\">changes</a>'"
+            let exp =
+              [[HyperLinkKind
+                  (StaticStmts [CallSt ("GT", [Val (String [[StringKind "changes"]])])],
+                   [[StringKind "changes"]])]]
+            Assert.Equal("", Right exp, runEither input)
+        testCase "test '<a href=\"exec: ''<<''x''>>''\">action</a>'" <| fun () ->
+            let input = "'<a href=\"exec: ''<<''x''>>''\">action</a>'"
+            let exp =
+                [[HyperLinkKind (Raw " '<<'x'>>'", [[StringKind "action"]])]]
+            Assert.Equal("", Right exp, runEither input)
+    ]
+[<Tests>]
+let pbracesTests =
+    let runEither str =
+        Qsp.Parser.Generic.runStateEither
+            (pbraces Tokens.TokenType.StringBraced)
+            Qsp.Parser.Generic.emptyState
+            str
+        |> snd
+    testList "stringLiteralWithTokenTest" [
+        testCase "base" <| fun () ->
+            Assert.Equal("", Right "", runEither "{}")
+        testCase "braces1" <| fun () ->
+            Assert.Equal("", Right "abc", runEither "{abc}")
+        testCase "1" <| fun () ->
+            let input =
+                [
+                    "{"
+                    "    asdf"
+                    "    {"
+                    "        asdf"
+                    "    }"
+                    "}"
+                ] |> String.concat "\n"
+            let exp =
+                [
+                    ""
+                    "    asdf"
+                    "    {"
+                    "        asdf"
+                    "    }"
+                    ""
+                ] |> String.concat "\n"
+            Assert.Equal("", Right exp, runEither input)
+    ]
+
+
+[<Tests>]
+let pcallProcTests =
+    let runStmts str =
+        Qsp.Parser.Generic.runStateEither Qsp.Parser.Main.pcallProc Qsp.Parser.Generic.emptyState str
+        |> snd
+    testList "pcallProcTests" [
+        testCase "pcallProcTests base" <| fun () ->
+            let input = "someProc arg1"
+            let exp =
+                CallSt ("someProc", [Var (ImplicitNumericType, "arg1")])
+
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "pcallProcTests base many args" <| fun () ->
+            let input = "someProc z / 2, x + y"
+            let exp =
+                (CallSt
+                   ("someProc",
+                    [Expr (Divide, Var (ImplicitNumericType, "z"), Val (Int 2));
+                     Expr
+                       (Plus, Var (ImplicitNumericType, "x"), Var (ImplicitNumericType, "y"))]))
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "pcallProcTests false with space" <| fun () ->
+            let input = "someProc "
+            // let exp =
+            //     [
+            //         "Error in Ln: 1 Col: 1"
+            //         "someProc "
+            //         "^"
+            //         ""
+            //         "The parser backtracked after:"
+            //         "  Error in Ln: 1 Col: 10"
+            //         "  someProc "
+            //         "           ^"
+            //         "  Note: The error occurred at the end of the input stream."
+            //         "  Expecting: identifier, integer number (32-bit, signed), prefix operator, '\"',"
+            //         "  '#', '$', '\\'', '(', '_' or '{'"
+            //         ""
+            //     ] |> String.concat "\r\n"
+            // Assert.Equal("", Left exp, runStmts input)
+            let act =
+                runStmts input
+                |> Option.ofEither
+            Assert.None("", act)
+        testCase "pcallProcTests false" <| fun () ->
+            let input = "someProc"
+            // let exp =
+            //     [
+            //         "Error in Ln: 1 Col: 1"
+            //         "someProc"
+            //         "^"
+            //         ""
+            //         "The parser backtracked after:"
+            //         "  Error in Ln: 1 Col: 9"
+            //         "  someProc"
+            //         "          ^"
+            //         "  Note: The error occurred at the end of the input stream."
+            //         "  Unknown Error(s)"
+            //         ""
+            //     ] |> String.concat "\r\n"
+            // Assert.Equal("", Left exp, runStmts input)
+            let act =
+                runStmts input
+                |> Option.ofEither
+            Assert.None("", act)
+        testCase "*pl" <| fun () ->
+            let input = "*pl"
+            let exp = CallSt ("*pl", [])
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "*pl arg1, arg2" <| fun () ->
+            let input = "*pl arg1, arg2"
+            let exp =
+                (CallSt
+                   ("*pl",
+                    [Var (ImplicitNumericType, "arg1"); Var (ImplicitNumericType, "arg2")]))
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "call `p2 x`, который начинается на заданный оператор `p`, но образует новый" <| fun () ->
+            let input = "p2 x"
+            let exp =
+                CallSt ("p2", [Var (ImplicitNumericType, "x")])
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "call ad-hoc `add obj`" <| fun () ->
+            let input = "add obj"
+            let exp =
+                CallSt ("addobj", [])
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "call ad-hoc `close all`" <| fun () ->
+            let input = "close all"
+            let exp =
+                CallSt ("close all", [])
+            Assert.Equal("", Right exp, runStmts input)
+    ]
+
+let printStmts stmts =
+    List.collect (Show.showStmt (Qsp.Show.UsingSpaces 4) Show.FormatConfig.Default) stmts
+    |> ShowList.joinEmpty "\n"
+    |> ShowList.show
+let printStmt stmt =
+    Qsp.Show.showStmt (Qsp.Show.UsingSpaces 4) Show.FormatConfig.Default stmt
+    |> ShowList.joinEmpty "\n"
+    |> ShowList.show
+[<Tests>]
+let ifTests =
+    let runStmts str =
+        Qsp.Parser.Generic.runStateEither
+            Qsp.Parser.Main.pstmt
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    let runStmtsEof str =
+        Qsp.Parser.Generic.runStateEither
+            (Qsp.Parser.Main.pstmt .>> eof)
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    testList "ifTests" [
+        testCase "inline if" <| fun () ->
+            let input =
+                [
+                    "if expr: gt 'hall'"
+                    "'statement that not belong to construction'"
+                ] |> String.concat "\n"
+            let exp =
+                (If
+                   (Var (ImplicitNumericType, "expr"), [CallSt ("gt", [Val (String [[StringKind "hall"]])])],
+                    []))
+            Assert.Equal("", Right exp, runStmts input)
+        testCase "inline if 2" <| fun () ->
+            let input =
+                [
+                    "if expr:"
+                    "    if expr2: stmt1"
+                    "    if expr3:"
+                    "        stmt1"
+                    "    else stmt2"
+                    "    if expr4: stmt3"
+                    "elseif expr5:"
+                    "    stmt6"
+                    "elseif expr6: stmt4"
+                ] |> String.concat "\n"
+            // tested
+            let exp =
+                If
+                  (Var (ImplicitNumericType, "expr"),
+                   [If
+                      (Var (ImplicitNumericType, "expr2"),
+                       [StarPl (Var (ImplicitNumericType, "stmt1"))], []);
+                    If
+                      (Var (ImplicitNumericType, "expr3"),
+                       [StarPl (Var (ImplicitNumericType, "stmt1"))],
+                       [StarPl (Var (ImplicitNumericType, "stmt2"))]);
+                    If
+                      (Var (ImplicitNumericType, "expr4"),
+                       [StarPl (Var (ImplicitNumericType, "stmt3"))], [])],
+                   [If
+                      (Var (ImplicitNumericType, "expr5"),
+                       [StarPl (Var (ImplicitNumericType, "stmt6"))],
+                       [If
+                          (Var (ImplicitNumericType, "expr6"),
+                           [StarPl (Var (ImplicitNumericType, "stmt4"))], [])])])
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "simple if" <| fun () ->
+            let input =
+                [
+                    "if expr:"
+                    "    someStmt"
+                    "end"
+                ] |> String.concat "\n"
+            let exp =
+                (If
+                   (Var (ImplicitNumericType, "expr"),
+                    [StarPl (Var (ImplicitNumericType, "someStmt"))], []))
+
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "elseif test" <| fun () ->
+            let input =
+                [
+                    "if expr1:"
+                    "    stmt1"
+                    "elseif expr2:"
+                    "    stmt2"
+                    "elseif expr3:"
+                    "    stmt3"
+                    "else"
+                    "    stmt4"
+                    "end"
+                ] |> String.concat "\n"
+            let exp =
+                (If
+                   (Var (ImplicitNumericType, "expr1"),
+                    [StarPl (Var (ImplicitNumericType, "stmt1"))],
+                    [If
+                       (Var (ImplicitNumericType, "expr2"),
+                        [StarPl (Var (ImplicitNumericType, "stmt2"))],
+                        [If
+                           (Var (ImplicitNumericType, "expr3"),
+                            [StarPl (Var (ImplicitNumericType, "stmt3"))],
+                            [StarPl (Var (ImplicitNumericType, "stmt4"))])])]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "elseif test2" <| fun () ->
+            let input =
+                [
+                    "if expr1:"
+                    "    stmt1"
+                    "elseif expr2:"
+                    "    stmt2"
+                    "elseif expr3:"
+                    "    stmt3"
+                    "end"
+                ] |> String.concat "\n"
+            let exp =
+                (If
+                   (Var (ImplicitNumericType, "expr1"),
+                    [StarPl (Var (ImplicitNumericType, "stmt1"))],
+                    [If
+                       (Var (ImplicitNumericType, "expr2"),
+                        [StarPl (Var (ImplicitNumericType, "stmt2"))],
+                        [If
+                           (Var (ImplicitNumericType, "expr3"),
+                            [StarPl (Var (ImplicitNumericType, "stmt3"))], [])])]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "another inline if" <| fun () ->
+            let input =
+                [
+                    "if expr:"
+                    "elseif expr: stmt"
+                ] |> String.concat "\n"
+            let exp =
+              (If
+                 (Var (ImplicitNumericType, "expr"), [],
+                  [If
+                     (Var (ImplicitNumericType, "expr"),
+                      [StarPl (Var (ImplicitNumericType, "stmt"))], [])]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "elseif test2" <| fun () ->
+            let input =
+                [
+                    "if expr1:"
+                    "    stmt1"
+                    "elseif expr2:"
+                    "    stmt2"
+                    "    if expr4:"
+                    "        stmt4"
+                    "    elseif expr5:"
+                    "        stmt5"
+                    "    end"
+                    "    stmt6"
+                    "elseif expr3:"
+                    "    stmt3"
+                    "end"
+                ] |> String.concat "\n"
+            let exp =
+              (If
+                 (Var (ImplicitNumericType, "expr1"),
+                  [StarPl (Var (ImplicitNumericType, "stmt1"))],
+                  [If
+                     (Var (ImplicitNumericType, "expr2"),
+                      [StarPl (Var (ImplicitNumericType, "stmt2"));
+                       If
+                         (Var (ImplicitNumericType, "expr4"),
+                          [StarPl (Var (ImplicitNumericType, "stmt4"))],
+                          [If
+                             (Var (ImplicitNumericType, "expr5"),
+                              [StarPl (Var (ImplicitNumericType, "stmt5"))], [])]);
+                       StarPl (Var (ImplicitNumericType, "stmt6"))],
+                      [If
+                         (Var (ImplicitNumericType, "expr3"),
+                          [StarPl (Var (ImplicitNumericType, "stmt3"))], [])])]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "if" <| fun () ->
+            let input =
+                [
+                    "if expr1:"
+                    "    stmt1"
+                    "    act 'arg': pl"
+                    "elseif expr2:"
+                    "    if expr3: stmt2 else stmt3 if expr4: stmt4 elseif expr5: stmt5"
+                    "    stmt6"
+                    "end"
+                ] |> String.concat "\n"
+
+            let exp =
+              (If
+                 (Var (ImplicitNumericType, "expr1"),
+                  [StarPl (Var (ImplicitNumericType, "stmt1"));
+                   Act ([Val (String [[StringKind "arg"]])], [CallSt ("pl", [])])],
+                  [If
+                     (Var (ImplicitNumericType, "expr2"),
+                      [If
+                         (Var (ImplicitNumericType, "expr3"),
+                          [StarPl (Var (ImplicitNumericType, "stmt2"))],
+                          [StarPl (Var (ImplicitNumericType, "stmt3"));
+                           If
+                             (Var (ImplicitNumericType, "expr4"),
+                              [StarPl (Var (ImplicitNumericType, "stmt4"))],
+                              [If
+                                 (Var (ImplicitNumericType, "expr5"),
+                                  [StarPl (Var (ImplicitNumericType, "stmt5"))], [])])]);
+                       StarPl (Var (ImplicitNumericType, "stmt6"))], [])]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+    ]
+
+[<Tests>]
+let forTests =
+    let runStmts str =
+        Qsp.Parser.Generic.runStateEither
+            Qsp.Parser.Main.pstmt
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    let runStmtsEof str =
+        Qsp.Parser.Generic.runStateEither
+            (Qsp.Parser.Main.pstmt .>> eof)
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    testList "forTests" [
+        testCase "multiline `for i = 4 + x to 45 / x + y:`" <| fun () ->
+            let input =
+                [
+                    "for i = 4 + x to 45 / x + y:"
+                    "    stmt"
+                    "end"
+                ] |> String.concat "\n"
+            let exp =
+              (For
+                 ((ImplicitNumericType, "i"),
+                  Expr (Plus, Val (Int 4), Var (ImplicitNumericType, "x")),
+                  Expr
+                    (Plus, Expr (Divide, Val (Int 45), Var (ImplicitNumericType, "x")),
+                     Var (ImplicitNumericType, "y")),
+                  [StarPl (Var (ImplicitNumericType, "stmt"))]))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "inline `for i = 4 + x to 45 / x + y: stmt`" <| fun () ->
+            let input =
+                [
+                    "for i = 4 + x to 45 / x + y: stmt"
+                    "'statement that not belong to construction'"
+                ] |> String.concat "\n"
+            let exp =
+              (For
+                 ((ImplicitNumericType, "i"),
+                  Expr (Plus, Val (Int 4), Var (ImplicitNumericType, "x")),
+                  Expr
+                    (Plus, Expr (Divide, Val (Int 45), Var (ImplicitNumericType, "x")),
+                     Var (ImplicitNumericType, "y")),
+                  [StarPl (Var (ImplicitNumericType, "stmt"))]))
+            Assert.Equal("", Right exp, runStmts input)
+    ]
+[<Tests>]
+let stmtTests =
+    let runStmts str =
+        Qsp.Parser.Generic.runStateEither
+            Qsp.Parser.Main.pstmt
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    let runStmtsEof str =
+        Qsp.Parser.Generic.runStateEither
+            (Qsp.Parser.Main.pstmt .>> eof)
+            Qsp.Parser.Generic.emptyState str
+        |> snd
+    testList "stmtTests" [
+        testCase "inline act" <| fun () ->
+            let input =
+                [
+                    "act 'some act': gt 'hall'"
+                    "'statement that not belong to construction'"
+                ] |> String.concat "\n"
+            let exp =
+                Act ([Val (String [[StringKind "some act"]])], [CallSt ("gt", [Val (String [[StringKind "hall"]])])])
+
+            Assert.Equal("", Right exp, runStmts input)
+
+        // порядок разбора
+        testCase "stmt `years -= 10`" <| fun () ->
+            let input = "years -= 10"
+            let exp =
+              (Assign
+                 (AssignVar (ImplicitNumericType, "years"),
+                  Expr (Minus, Var (ImplicitNumericType, "years"), Val (Int 10))))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "call function as expression" <| fun () ->
+            // f(1) — должно обрабатываться раньше, чем `callProc arg1, arg2`
+            let input = "iif(somevar >= 2, 'thenBody', 'elseBody')"
+            let exp =
+              (StarPl
+                 (Func
+                    ("iif",
+                     [Expr (Ge, Var (ImplicitNumericType, "somevar"), Val (Int 2));
+                      Val (String [[StringKind "thenBody"]]); Val (String [[StringKind "elseBody"]])])))
+            Assert.Equal("", Right exp, runStmtsEof input)
+        testCase "call procedure" <| fun () ->
+            let input = "gt 'begin', 'real_character'"
+            let exp =
+                CallSt ("gt", [Val (String [[StringKind "begin"]]); Val (String [[StringKind "real_character"]])])
+            Assert.Equal("", Right exp, runStmtsEof input)
+        // testCase "call " <| fun () ->
+        //     let input = "The(Lady), or, the, Tiger"
+        //     let exp =
+        //         CallSt ("gt", [Val (String "begin"); Val (String "real_character")])
+        //     Assert.Equal("", Right exp, runStmts input)
+    ]
+
+module TestOnMocks =
+    type T = Location list
+    let enc = System.Text.Encoding.UTF8
+    let startOnFile path =
+        match Qsp.Parser.Main.startOnFile enc path with
+        | Success(x, _, _) -> x
+        | Failure(x, _, _) -> failwithf "%s\n%s" path x
+    let replaceOrNot expPath actPath =
+        printfn "\"%s\"\nnot equal\n\"%s\""
+            (System.IO.Path.GetFullPath expPath)
+            (System.IO.Path.GetFullPath actPath)
+        let rec whileYOrN () =
+            match System.Console.ReadKey().Key with
+            | System.ConsoleKey.Y -> true
+            | System.ConsoleKey.N -> false
+            | x ->
+                printfn "need (y/n) but %A" x
+                whileYOrN ()
+        printfn "Replace? (y/n)"
+        let res = whileYOrN()
+        if res then
+            System.IO.File.Copy(actPath, expPath, true)
+            printfn "replaced"
+        res
+    let addExpToPath path =
+        path
+        |> Path.changeFileNameWithoutExt (sprintf "%sExp")
+    let outputDir = @"..\..\..\Mocks"
+    let copyAsExp path =
+        System.IO.File.Copy(path, addExpToPath path, true)
+    let getPathActLocal pathAct =
+        sprintf "%s\\%s" outputDir (System.IO.Path.GetFileName pathAct)
+        |> fun x -> System.IO.Path.ChangeExtension(x, ".json")
+    let showTest path =
+        let srcPath = path
+        let parseActPath = getPathActLocal srcPath
+        let parseExpPath = addExpToPath parseActPath
+        let getPath path =
+            sprintf "%s\\%s" outputDir (System.IO.Path.GetFileName path)
+            |> fun x -> System.IO.Path.ChangeExtension(x, ".qsps")
+        let showActPath = getPath srcPath
+        let showExpPath = addExpToPath showActPath
+
+        let act =
+            // if System.IO.File.Exists parseExpPath then
+            //     let src : T = Json.desf parseExpPath
+            //     src |> Qsp.Show.printLocs Qsp.Show.UsingTabs
+            // else
+                let act = startOnFile srcPath
+                // act |> Json.serf parseExpPath
+                // failwithf "\"%s\" не найден, потому пришлось его создать. Естественно, все тесты пошли коту под хвост." parseExpPath
+                act |> Qsp.Show.printLocs Qsp.Show.UsingTabs Show.FormatConfig.Default
+        let exp =
+            if System.IO.File.Exists showExpPath then
+                System.IO.File.ReadAllText showExpPath
+            else
+                System.IO.File.WriteAllText(showExpPath, act)
+                failwithf "\"%s\" не найден, потому пришлось его создать. Естественно, все тесты пошли коту под хвост." showExpPath
+        if exp <> act then
+            System.IO.File.WriteAllText(showActPath, act)
+
+            if replaceOrNot showExpPath showActPath then ()
+            else failwithf "not pass"
+    let mockTestList = "mock tests"
+    [<Tests>]
+    let showTests =
+        let mocksDir = outputDir + @"\Src"
+        let tests =
+            if System.IO.Directory.Exists mocksDir then
+                System.IO.Directory.GetFiles(mocksDir, "*.qsps")
+                |> Array.map (fun path ->
+                    testCase (sprintf "'%s' test" (System.IO.Path.GetFullPath path)) <| fun () ->
+                        showTest path
+                        Assert.Equal("", true, true)
+                )
+            else [||]
+        testList mockTestList tests
 
 [<EntryPoint;System.STAThread>]
-let main arg =
-    defaultMainThisAssembly arg
+let main args =
+    let isFullTest () =
+        let rec whileYOrN () =
+            match System.Console.ReadKey().Key with
+            | System.ConsoleKey.Y -> true
+            | System.ConsoleKey.N -> false
+            | x ->
+                printfn "`y` or `n` but %A" x
+                whileYOrN ()
+        printfn "Full test? (`y` or `n`)"
+        whileYOrN ()
+    let f isFullTest =
+        if isFullTest then
+            defaultMainThisAssembly args
+        else
+            defaultMainThisAssemblyFilter args
+                (fun x ->
+                    x.Where(fun x -> not <| x.StartsWith TestOnMocks.mockTestList))
+    match args with
+    | [|"--full"|] -> f true
+    | [||] ->
+        f (isFullTest ())
+    | _ ->
+        printfn "`--full` or pass args but: %A" args
+        1

+ 5 - 1
Test/Test.fsproj

@@ -7,17 +7,21 @@
     <Tailcalls>true</Tailcalls>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\QSParse\QSParse.fsproj">
+      <Name>QSParse.fsproj</Name>
+    </ProjectReference>
     <ProjectReference Include="..\QspServer\QspServer.fsproj">
       <Name>QspServer.fsproj</Name>
     </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <None Include="App.config" />
+    <Compile Include="OldAndNewExprTests.fs" />
     <Compile Include="Test.fs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="System.Runtime" />
-    <Reference Include="..\paket-files\github.com\gretmn102\FParserQSP\paket-files\github.com\gretmn102\FsharpMyExtension\FsharpMyExtension\FsharpMyExtension\bin\Release\net461\FsharpMyExtension.dll" />
+    <Reference Include="..\paket-files\github.com\gretmn102\FsharpMyExtension\FsharpMyExtension\FsharpMyExtension\bin\Debug\net461\FsharpMyExtension.dll" />
   </ItemGroup>
   <Import Project="..\.paket\Paket.Restore.targets" />
 </Project>

+ 1 - 1
Test/paket.references

@@ -1,2 +1,2 @@
 FSharp.Core
-FuChu
+FuChu

+ 40 - 7
build.fsx

@@ -20,6 +20,7 @@ let f projName =
 let testProjName = "Test"
 let testProjPath = @"Test\Test.fsproj"
 let mainProjName = "QspServer"
+let mainProjName2 = "QSParse"
 let mainProjPath = f mainProjName
 // --------------------------------------------------------------------------------------
 // Helpers
@@ -57,16 +58,14 @@ Target.create "RunTest" (fun _ ->
         raise <| Fake.Testing.Common.FailedTestsException "test error"
 )
 
-Target.create "RunMainProj" (fun _ ->
-    run mainProjName mainProjPath |> ignore
-)
-
 Target.create "TrimTrailingWhitespace" (fun _ ->
     // по-хорошему, нужно использовать .gitignore, но и так пока сойдет
     let files =
         !! "**/*.fs"
         ++ "**/*.fsx"
         ++ "**/*.fsproj"
+        ++ "**/*.cs"
+        ++ "**/*.csproj"
         -- "**/obj/**"
         -- "**/paket-files/**"
         -- "**/packages/**"
@@ -78,6 +77,42 @@ Target.create "TrimTrailingWhitespace" (fun _ ->
     )
 )
 
+open Fake.IO
+// TODO: скачать и распаковать http://qsp.su/attachments/txt2gam011.zip в "Utils\txt2gam"
+let compilerPath =
+    Lazy.Create (fun _ ->
+        let compilerPath = "Utils/txt2gam/txt2gam.exe"
+        if System.IO.File.Exists compilerPath then compilerPath
+        else
+            failwithf "Compiler not found at '%A'" compilerPath
+    )
+let compiler src =
+    let dst = Path.changeExtension ".qsp" src
+
+    Command.RawCommand(compilerPath.Value, Arguments.ofList [src; dst])
+    |> CreateProcess.fromCommand
+    |> CreateProcess.withWorkingDirectory (Path.getDirectory compilerPath.Value)
+    |> Proc.run
+    |> fun x -> x.ExitCode
+
+Target.create "Watch" (fun _ ->
+    use watcher =
+       !! "Utils/txt2gam/*.qsps"
+       |> Fake.IO.ChangeWatcher.run (fun changes ->
+           changes
+           |> Seq.iter (fun x ->
+               Trace.trace "Compilation..."
+               let code = compiler x.FullPath
+               Trace.trace (sprintf "Compilation completed with code %d" code)
+           )
+    )
+
+    System.Console.ReadLine() |> ignore
+
+    watcher.Dispose() // по-идеи, и так должен выгрузиться
+)
+
+
 // --------------------------------------------------------------------------------------
 // Build order
 // --------------------------------------------------------------------------------------
@@ -85,6 +120,4 @@ open Fake.Core.TargetOperators
 
 "BuildTest"
   ==> "RunTest"
-  ==> "RunMainProj"
-
-Target.runOrDefault "RunMainProj"
+Target.runOrDefault "RunTest"

+ 1 - 1
paket.dependencies

@@ -10,7 +10,7 @@ nuget Newtonsoft.Json
 
 // https://github.com/fsharp/FsAutoComplete/tree/master/src/LanguageServerProtocol
 github fsharp/FsAutoComplete src/LanguageServerProtocol/LanguageServerProtocol.fs
-git https://github.com/gretmn102/FParserQSP.git build:"build.cmd"
+git https://github.com/gretmn102/FsharpMyExtension.git build:"build.cmd"
 
 group Build
   source https://nuget.org/api/v2

+ 2 - 2
paket.lock

@@ -15,8 +15,8 @@ GITHUB
   remote: fsharp/FsAutoComplete
     src/LanguageServerProtocol/LanguageServerProtocol.fs (07a0e745eea71f347040efc5f8c72c1d42ddf117)
 GIT
-  remote: https://github.com/gretmn102/FParserQSP.git
-     (353e7a7ff66cf412c2c16759a2bbf3fc7c4f23ff)
+  remote: https://github.com/gretmn102/FsharpMyExtension.git
+     (eff1014adc2f0aca97a72af5c28235ac34cf30d7)
       build: build.cmd
 GROUP Build
 NUGET