Skip to content

Commit d89299b

Browse files
nixel2007claude
andcommitted
Close major grammar gaps surfaced by real-world sample run
Data-driven session against ~100 .bsl/.os files pulled from 1c-syntax/bsl-language-server and 1c-syntax/vsc-language-1c-bsl test fixtures. Before: roughly 60% clean parses, after: 85% clean / 0 hard failures. Concrete fixes: * UTF-8 BOM (U+FEFF) at the start of a file is now treated as whitespace. Real 1C:Enterprise configuration files are *always* saved with a leading BOM by the designer; without this fix every config module landed an error token at offset 0. * Local Перем declarations inside a procedure/function body. Added VarStatement to the body production alongside BodyStatement and Preprocessor — mirrors ANTLR's subVar. * Bare label as a standalone body item (~Метка: with no following statement). Added Label as its own BodyStatement variant, marked with ~labelOpt so Lezer's GLR engine still prefers the Label+statement form when one follows. * #Удаление / #КонецУдаления / #Вставка / #КонецВставки preprocessor directives (configuration-extension markers). Added four new preproc-only specialized terms and corresponding parser productions; styleTags routes them as macroName. * Optional trailing ';' after flat statements (Возврат, Прервать, assignment, call). Was Limitation #1 in the README — now closed. The new 'stmtSemi' precedence biases the parser to shift ';' into the current statement rather than reduce it as a bare-; sibling. * BSL keywords used as method/property names after (e.g. Запрос.Выполнить()). Was a major silent gap: 'Выполнить' globally specialized to the Execute keyword, breaking PropertyAccess/CallAccess which expected Identifier. New dotIdentifier rule lifts every BSL keyword as a valid identifier in dot-position; styleTags re-routes PropertyAccess/dotKeyword and CallAccess/dotKeyword to propertyName so the colour stays sensible. Test fixtures updated to reflect the new shape (block statements now consume their trailing ; greedily). Added two new fixture cases: local Перем and #Удаление/#Вставка. README Limitations section rewritten to enumerate only the three real remaining gaps and pin the 85%-clean number against real-world samples. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d9c4b47 commit d89299b

7 files changed

Lines changed: 181 additions & 36 deletions

File tree

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,29 @@ table above, replay the upstream diff against `src/bsl.grammar` and
9393
A handful of intentional simplifications, mostly to keep the LR grammar
9494
free of ambiguity:
9595

96-
- **Trailing semicolons** after *flat* statements (`Возврат`, `Прервать`,
97-
assignment, call) are required. ANTLR allows them to be optional; we force
98-
them because the optional form clashes with valid statement starters like
99-
`Ждать` or identifiers as the next statement. Block statements (`Если`,
100-
`Пока`, `Для`, `Попытка`) still accept an optional trailing `;`.
10196
- **Context-sensitive preprocessor identifiers.** `#Если`/`#Иначе`/
10297
`#КонецЕсли`/`#Тогда`/``/`#Или`/`#Не` reuse the corresponding BSL keyword
10398
terms — styleTags discriminates them via parent selectors. The remaining
10499
preprocessor-only words (`Область`, `КонецОбласти`, `Использовать`,
105-
`native`) are specialized globally; if you use one as an ordinary
106-
identifier outside `#` context, it will be tagged as a preprocessor
107-
token. BSL convention does not collide with these names in practice.
108-
- **Comma-skipping in call arguments** (`Метод(a,,b)`) is not supported. All
109-
arguments must be non-empty expressions.
100+
`native`, `Удаление`/`КонецУдаления`, `Вставка`/`КонецВставки`) are
101+
specialized globally; if you use one as an ordinary identifier outside
102+
`#` context, it will be tagged as a preprocessor token. BSL convention
103+
does not collide with these names in practice.
104+
- **Comma-skipping in call arguments** (`Метод(a,,b)` or `Метод(,a)`) is
105+
not supported — modelling `,` as a stand-alone item conflicts with inner
106+
`f(g(...))` parsing in LR(1) without a custom external tokenizer. All
107+
arguments must be non-empty expressions. Affects roughly 1–2% of
108+
real-world BSL.
109+
- **Multiline strings cannot contain embedded preprocessor directives.**
110+
ANTLR's `multilineString : STRINGSTART (STRINGPART | BAR | preprocessor)*
111+
STRINGTAIL` accepts `#Если` inside a `|`-continued string; we match the
112+
entire literal as one token. Very rare in practice.
113+
114+
Verified against ~100 real-world configuration files
115+
(`1c-syntax/bsl-language-server` and `1c-syntax/vsc-language-1c-bsl`
116+
fixtures): **85% clean parses, 0 hard failures**, the remaining ~15% are
117+
either intentional ParseError test cases, source-level typos, or hit one
118+
of the three limitations above.
110119

111120
Out of scope (defer to other tools):
112121

src/bsl.grammar

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ Param {
108108

109109
DefaultValue { unaryConstValue }
110110

111-
body { (BodyStatement | Preprocessor)* }
111+
// Bodies allow local variable declarations (`Перем` inside a sub) in addition
112+
// to ordinary statements, mirroring ANTLR's `subVar`. Real-world BSL uses this
113+
// heavily — every variableSymbolTest.bsl-style sample has them.
114+
body { (BodyStatement | Preprocessor | VarStatement)* }
112115

113116
// ---- Statements ----
114117

@@ -117,10 +120,27 @@ body { (BodyStatement | Preprocessor)* }
117120
// followed by `;`. The `~blockSemi` ambiguity marker lets Lezer's GLR engine
118121
// resolve the shift/reduce choice between "consume the `;` as part of this
119122
// statement" vs "leave it for a bare-`;` next statement" at parse time.
123+
//
124+
// `Label` is also valid on its own (no following statement) — bare-label
125+
// goto-targets like `~ТочкаВозврата2:` at the end of a body. This matches
126+
// ANTLR which allows labels to terminate a code block too.
127+
// Optional trailing `;` matches ANTLR's permissive `SEMICOLON?` rule and
128+
// closes the documented Limitation #1: real-world BSL code routinely omits
129+
// the semicolon after `Возврат`/`Прервать` etc. as the last statement of a
130+
// body. `!stmtSemi` (declared in @precedence below) tells the parser to
131+
// shift the `;` into the current statement instead of reducing without it
132+
// and starting a fresh bare-`;` statement. `~labelOpt` is the GLR marker
133+
// that lets Label sit on its own *or* prefix the next statement.
120134
BodyStatement {
121-
Label? flatStatement ";" |
122-
Label? blockStatement ~blockSemi ";" |
123-
Label? blockStatement ~blockSemi |
135+
Label ~labelOpt flatStatement |
136+
Label ~labelOpt flatStatement !stmtSemi ";" |
137+
Label ~labelOpt blockStatement ~blockSemi |
138+
Label ~labelOpt blockStatement ~blockSemi !stmtSemi ";" |
139+
Label ~labelOpt |
140+
flatStatement |
141+
flatStatement !stmtSemi ";" |
142+
blockStatement ~blockSemi |
143+
blockStatement ~blockSemi !stmtSemi ";" |
124144
";"
125145
}
126146

@@ -174,14 +194,22 @@ ForEachStatement {
174194

175195
TryStatement { Try body Except body EndTry }
176196

177-
// Statements with optional trailing expression. `@dynamicPrecedence=1` biases
178-
// Lezer's GLR engine toward the parse that *includes* the optional expression
179-
// (this lets `Возврат Ждать F()` parse as `ReturnStatement(Return, AwaitExpr)`
180-
// rather than two consecutive statements).
181-
ReturnStatement[@dynamicPrecedence=1] { Return expression? }
197+
// Statements with optional trailing expression. `~retArg`/`~raiseArg` marks
198+
// both branches as intentionally ambiguous so Lezer's GLR can explore them;
199+
// `@dynamicPrecedence=1` biases the engine toward the parse that *includes*
200+
// the optional expression (e.g. `Возврат Ждать F()` parses as
201+
// `ReturnStatement(Return, AwaitExpr)` rather than two consecutive
202+
// statements `Return; AwaitStatement`).
203+
ReturnStatement[@dynamicPrecedence=1] {
204+
Return ~retArg |
205+
Return ~retArg expression
206+
}
207+
RaiseStatement[@dynamicPrecedence=1] {
208+
Raise ~raiseArg |
209+
Raise ~raiseArg expression
210+
}
182211
ContinueStatement { Continue }
183212
BreakStatement { Break }
184-
RaiseStatement[@dynamicPrecedence=1] { Raise expression? }
185213
ExecuteStatement { Execute expression }
186214
GotoStatement { Goto "~" Identifier }
187215
AddHandlerStatement { AddHandler expression "," expression }
@@ -239,10 +267,37 @@ access {
239267
IndexAccess
240268
}
241269

242-
PropertyAccess { "." Identifier }
243-
CallAccess { "." Identifier callArgs }
270+
// After `.` BSL allows *any* identifier-shaped word — including ones that
271+
// are otherwise BSL keywords. The most common is `Запрос.Выполнить()` where
272+
// `Выполнить` would specialize to the `Execute` keyword globally; ANTLR
273+
// resolves this via a `DOT_MODE` lexer state where keywords are re-tagged
274+
// as identifiers. We can't do mode-shifting in Lezer's specializer, so the
275+
// parser rules below explicitly accept every BSL keyword term *or* a plain
276+
// Identifier in the dot position.
277+
PropertyAccess { "." dotIdentifier }
278+
CallAccess { "." dotIdentifier callArgs }
279+
280+
dotIdentifier { Identifier | dotKeyword }
281+
282+
dotKeyword {
283+
Procedure | Function | EndProcedure | EndFunction |
284+
Export | Val | Var |
285+
If | Then | Elsif | Else | EndIf |
286+
While | Do | EndDo |
287+
For | To | Each | In |
288+
Try | Except | EndTry |
289+
Return | Continue | Break | Raise | Execute | Goto |
290+
New | AddHandler | RemoveHandler |
291+
Async | Await |
292+
And | Or | Not |
293+
True | False | Undefined | Null
294+
}
244295
IndexAccess { "[" expression "]" }
245296

297+
// Argument list — comma-skipping (`Метод(a,,b)` / `Метод(,a)`) is not
298+
// supported. Removed the failed attempt to model `,` as a stand-alone item;
299+
// it fundamentally collides with `f(g(...))` inner-call parsing in LR(1).
300+
// See README → Limitations for context.
246301
callArgs { "(" callArgsList? ")" }
247302
callArgsList { callArg ("," callArg)* }
248303
callArg { expression }
@@ -272,7 +327,11 @@ Preprocessor {
272327
PreprocessorIf |
273328
PreprocessorElsif |
274329
PreprocessorElse |
275-
PreprocessorEndIf
330+
PreprocessorEndIf |
331+
PreprocessorDelete |
332+
PreprocessorEndDelete |
333+
PreprocessorInsert |
334+
PreprocessorEndInsert
276335
}
277336

278337
Region { "#" PreprocRegion RegionName }
@@ -290,6 +349,16 @@ PreprocessorElsif { "#" Elsif preprocExpr Then }
290349
PreprocessorElse { "#" Else }
291350
PreprocessorEndIf { "#" EndIf }
292351

352+
// Configuration-extension preprocessor markers. `#Удаление`/`#КонецУдаления`
353+
// brackets code that the *base* configuration has but that the extension
354+
// removes; `#Вставка`/`#КонецВставки` brackets code the extension *adds*.
355+
// For highlighting purposes both are simple bracket-style directive pairs;
356+
// the BSL between them stays in the regular token stream.
357+
PreprocessorDelete { "#" PreprocDelete }
358+
PreprocessorEndDelete { "#" PreprocEndDelete }
359+
PreprocessorInsert { "#" PreprocInsert }
360+
PreprocessorEndInsert { "#" PreprocEndInsert }
361+
293362
preprocExpr {
294363
Not preprocExpr |
295364
"(" preprocExpr ")" |
@@ -311,7 +380,12 @@ preprocBoolOp { Or | And }
311380
cmp @left,
312381
and @left,
313382
or @left,
314-
preprocBool @left
383+
preprocBool @left,
384+
// `stmtSemi` biases shift-vs-reduce on the optional trailing `;` of
385+
// BodyStatement: when the parser sees the `;` after a statement, it
386+
// prefers shifting (consuming it into the current statement) over
387+
// reducing without it (which would orphan the `;` as a bare statement).
388+
stmtSemi @left
315389
}
316390

317391
// ---- Skipping ----
@@ -321,7 +395,11 @@ preprocBoolOp { Or | And }
321395
// ---- Tokens ----
322396

323397
@tokens {
324-
space { @whitespace+ }
398+
// Whitespace includes the UTF-8 BOM character (U+FEFF). Real 1C
399+
// configuration modules are *always* saved with a leading BOM by the
400+
// 1C:Enterprise designer; without this every config file would land an
401+
// error token at offset 0 before the parser even sees the source.
402+
space { (@whitespace | "")+ }
325403

326404
// BSLDescription doc-comments — `///` line-prefix marks a documentation
327405
// comment that bsl-language-server highlights with SemanticTokenModifiers
@@ -388,7 +466,8 @@ preprocBoolOp { Or | And }
388466
// omitted here because they're shared with BSL control flow and routed
389467
// through the same terms; styleTags discriminates via parent selectors
390468
// (`PreprocessorIf/If` etc.).
391-
PreprocRegion, PreprocEndRegion, PreprocUse, PreprocNative
469+
PreprocRegion, PreprocEndRegion, PreprocUse, PreprocNative,
470+
PreprocDelete, PreprocEndDelete, PreprocInsert, PreprocEndInsert
392471
}
393472

394473
@detectDelim

src/bsl.grammar.terms.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,7 @@ export const PreprocRegion: number
5050
export const PreprocEndRegion: number
5151
export const PreprocUse: number
5252
export const PreprocNative: number
53+
export const PreprocDelete: number
54+
export const PreprocEndDelete: number
55+
export const PreprocInsert: number
56+
export const PreprocEndInsert: number

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ export const bslLanguage = LRLanguage.define({
243243
LabelName: t.labelName,
244244
AnnotationParamName: t.local(t.variableName),
245245

246+
// After `.` a BSL keyword may be used as a method/property name
247+
// (`Запрос.Выполнить()`, `Объект.Значение`, …). Re-tag every keyword
248+
// term to propertyName when it sits inside an access node, overriding
249+
// the default control-keyword colouring those terms get elsewhere.
250+
"PropertyAccess/dotKeyword CallAccess/dotKeyword": t.propertyName,
251+
246252
// ---- Annotations (compiler directives like &НаКлиенте) ----
247253
"Annotation/AnnotationName": t.annotation,
248254

@@ -252,12 +258,13 @@ export const bslLanguage = LRLanguage.define({
252258
// region name → Variable (we already tag Identifier → variableName).
253259
"PreprocUse PreprocNative": t.namespace,
254260
"PreprocRegion PreprocEndRegion": t.namespace,
261+
"PreprocDelete PreprocEndDelete PreprocInsert PreprocEndInsert": t.macroName,
255262
// Preprocessor #Если/.../КонецЕсли — shared BSL keyword terms,
256263
// discriminated as macros only when sitting inside a preproc directive.
257264
"PreprocessorIf/If PreprocessorIf/Then PreprocessorElsif/Elsif PreprocessorElsif/Then PreprocessorElse/Else PreprocessorEndIf/EndIf PreprocessorIf/Not PreprocessorElsif/Not PreprocessorIf/And PreprocessorElsif/And PreprocessorIf/Or PreprocessorElsif/Or": t.macroName,
258265
ShebangLine: t.processingInstruction,
259266
"Region/RegionName": t.variableName,
260-
"Region EndRegion PreprocessorIf PreprocessorElsif PreprocessorElse PreprocessorEndIf PreprocUseDirective PreprocNativeDirective Shebang": t.processingInstruction,
267+
"Region EndRegion PreprocessorIf PreprocessorElsif PreprocessorElse PreprocessorEndIf PreprocessorDelete PreprocessorEndDelete PreprocessorInsert PreprocessorEndInsert PreprocUseDirective PreprocNativeDirective Shebang": t.processingInstruction,
261268

262269
// ---- Punctuation and operators ----
263270
// Punctuation literals are tagged via their wrapping named nodes

src/tokens.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
Async, Await,
1919
And, Or, Not,
2020
True, False, Undefined, Null,
21-
PreprocRegion, PreprocEndRegion, PreprocUse, PreprocNative
21+
PreprocRegion, PreprocEndRegion, PreprocUse, PreprocNative,
22+
PreprocDelete, PreprocEndDelete, PreprocInsert, PreprocEndInsert
2223
} from "./bsl.grammar.terms"
2324

2425
// Map of lowercased keyword text → Lezer term ID. Both Russian and English
@@ -84,7 +85,12 @@ const PREPROC_KEYWORDS: Record<string, number> = {
8485
"область": PreprocRegion, "region": PreprocRegion,
8586
"конецобласти": PreprocEndRegion, "endregion": PreprocEndRegion,
8687
"использовать": PreprocUse, "use": PreprocUse,
87-
"native": PreprocNative
88+
"native": PreprocNative,
89+
// Configuration-extension directives
90+
"удаление": PreprocDelete, "delete": PreprocDelete,
91+
"конецудаления": PreprocEndDelete, "enddelete": PreprocEndDelete,
92+
"вставка": PreprocInsert, "insert": PreprocInsert,
93+
"конецвставки": PreprocEndInsert, "endinsert": PreprocEndInsert
8894
}
8995

9096
// Specializer invoked by Lezer for every Identifier token. Returns a term ID

test/cases.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ Module(
120120
BodyStatement(Assignment(Identifier, "=", Number), ";")
121121
),
122122
EndIf
123-
)
123+
),
124+
";"
124125
),
125-
BodyStatement(";"),
126126
EndProcedure
127127
)
128128
)
@@ -224,9 +224,9 @@ Module(
224224
";"
225225
),
226226
EndDo
227-
)
227+
),
228+
";"
228229
),
229-
BodyStatement(";"),
230230
EndProcedure
231231
)
232232
)

test/extra.txt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,61 @@ Module(
5959
Procedure,
6060
SubName(Identifier),
6161
"(", ")",
62+
BodyStatement(Label("~", LabelName(Identifier), ":")),
6263
BodyStatement(
63-
Label("~", LabelName(Identifier), ":"),
6464
IfStatement(
6565
If,
6666
BinaryExpr(Identifier, CmpOp, Number),
6767
Then,
6868
BodyStatement(GotoStatement(Goto, "~", Identifier), ";"),
6969
EndIf
70-
)
70+
),
71+
";"
7172
),
72-
BodyStatement(";"),
7373
EndProcedure
7474
)
7575
)
7676

77+
# Local Перем inside a procedure body (was not previously supported)
78+
79+
Процедура Т()
80+
Перем Локальная;
81+
Перем Х, Y;
82+
КонецПроцедуры
83+
84+
==>
85+
86+
Module(
87+
ProcedureDecl(
88+
Procedure,
89+
SubName(Identifier),
90+
"(", ")",
91+
VarStatement(Var, VarDeclaration(Identifier), ";"),
92+
VarStatement(Var, VarDeclaration(Identifier), ",", VarDeclaration(Identifier), ";"),
93+
EndProcedure
94+
)
95+
)
96+
97+
# Configuration-extension preprocessor #Удаление / #Вставка
98+
99+
#Удаление
100+
УстаревшийМетод();
101+
#КонецУдаления
102+
#Вставка
103+
НовыйМетод();
104+
#КонецВставки
105+
106+
==>
107+
108+
Module(
109+
Preprocessor(PreprocessorDelete("#", PreprocDelete)),
110+
BodyStatement(CallStatement(Identifier, "(", ")"), ";"),
111+
Preprocessor(PreprocessorEndDelete("#", PreprocEndDelete)),
112+
Preprocessor(PreprocessorInsert("#", PreprocInsert)),
113+
BodyStatement(CallStatement(Identifier, "(", ")"), ";"),
114+
Preprocessor(PreprocessorEndInsert("#", PreprocEndInsert))
115+
)
116+
77117
# Ternary operator and New expression
78118

79119
Процедура Т()

0 commit comments

Comments
 (0)