Skip to content

Commit 39b33fc

Browse files
committed
[+] SimaiParser重写完成
1 parent 56f1872 commit 39b33fc

8 files changed

Lines changed: 106 additions & 33 deletions

File tree

Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ private static int Main(string[] args)
2626
{
2727
PrintAlerts(ex.Alerts, "转换失败:");
2828
Console.Error.WriteLine("转换失败!报错详见如上。您可以通过 https://github.com/MuNet-OSS/MuConvert/issues 反馈问题。");
29+
#if DEBUG
30+
Console.Error.WriteLine(ex);
31+
#endif
2932
return 1;
3033
}
3134
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)

i18n/Locale.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

i18n/Locale.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
<data name="RecoverInlineExtraneousToken" xml:space="preserve">
191191
<value>Extraneous token: {0} (typo?). Ignored.</value>
192192
</data>
193+
<data name="RecoverInlineExtraneousTokenStrict" xml:space="preserve">
194+
<value>Extraneous token: {0} (typo?)</value>
195+
</data>
193196
<data name="RecoverInlineMissingToken" xml:space="preserve">
194197
<value>Missing token {1} before {0} (typo?). Inserted automatically.</value>
195198
</data>

i18n/Locale.zh-hans.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
<data name="RecoverInlineExtraneousToken" xml:space="preserve">
191191
<value>多余的符号: {0} (是不是手滑按错了?),已忽略该符号。</value>
192192
</data>
193+
<data name="RecoverInlineExtraneousTokenStrict" xml:space="preserve">
194+
<value>多余的符号: {0} (是不是手滑按错了?)</value>
195+
</data>
193196
<data name="RecoverInlineMissingToken" xml:space="preserve">
194197
<value>在 {0} 前似乎缺失了符号 {1} (是不是漏打了?),已尝试帮您补全。</value>
195198
</data>

i18n/Locale.zh-hant.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
<data name="RecoverInlineExtraneousToken" xml:space="preserve">
191191
<value>多餘的符號: {0}(是否手誤?),已忽略該符號。</value>
192192
</data>
193+
<data name="RecoverInlineExtraneousTokenStrict" xml:space="preserve">
194+
<value>多餘的符號: {0}(是否手誤?)</value>
195+
</data>
193196
<data name="RecoverInlineMissingToken" xml:space="preserve">
194197
<value>在 {0} 前似乎缺失了符號 {1}(是否漏打?),已嘗試為您補全。</value>
195198
</data>

parser/simai/ErrorStrategy.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
using MuConvert.Antlr;
44
using MuConvert.utils;
55
using static MuConvert.utils.Alert.LEVEL;
6+
using L = MuConvert.Antlr.SimaiLexer;
67

7-
namespace MuConvert.parser;
8+
namespace MuConvert.parser.simai;
89

910
public class ErrorListener(SimaiParser simaiParser): BaseErrorListener, IAntlrErrorListener<int>
1011
{
@@ -18,14 +19,14 @@ public override void SyntaxError(TextWriter output, IRecognizer recognizer, ITok
1819
{
1920
simaiParser.alerts.Add(new Alert(Warning,
2021
string.Format(Locale.RecoverInlineExtraneousToken, GetTokenErrorDisplay(offendingSymbol)),
21-
line: line, relevantNote: _contextText(parser.Context)));
22+
line: line, relevantNote: RelevantNote(parser.Context)));
2223
return;
2324
}
2425
else if (msg.StartsWith("missing"))
2526
{
2627
simaiParser.alerts.Add(new Alert(Warning,
2728
string.Format(Locale.RecoverInlineMissingToken, GetTokenErrorDisplay(offendingSymbol), parser.GetExpectedTokens().ToString(parser.Vocabulary)),
28-
line: line, relevantNote: _contextText(parser.Context)));
29+
line: line, relevantNote: RelevantNote(parser.Context)));
2930
return;
3031
}
3132
}
@@ -48,7 +49,7 @@ public override void SyntaxError(TextWriter output, IRecognizer recognizer, ITok
4849
message = string.Format(Locale.AntlrUnknownError, msg);
4950
break;
5051
}
51-
simaiParser.alerts.Add(new Alert(level, message, line: line, relevantNote: _contextText(parser.Context)));
52+
simaiParser.alerts.Add(new Alert(level, message, line: line, relevantNote: RelevantNote(parser.Context)));
5253
}
5354

5455
// 词法分析的错误报告函数
@@ -73,7 +74,7 @@ public void SyntaxError(TextWriter output, IRecognizer recognizer, int offending
7374
}
7475

7576
// 从context获得为适合放进relevantNote里的形式
76-
private string? _contextText(RuleContext? context)
77+
private static string? RelevantNote(RuleContext? context)
7778
{
7879
while (true)
7980
{
@@ -98,26 +99,29 @@ private class _ES : DefaultErrorStrategy
9899
/**
99100
* 最宽松的ErrorStrategy,尽全力恢复不让谱面整个垮掉
100101
*/
101-
public class LaxErrorStrategy : DefaultErrorStrategy
102+
public class LaxErrorStrategy(SimaiParser simaiParser) : DefaultErrorStrategy
102103
{
103104
protected override IToken SingleTokenDeletion(Parser recognizer)
104105
{
105-
if (recognizer.CurrentToken?.Type == SimaiLexer.COMMA) return null!; // 不准删逗号
106+
if (recognizer.CurrentToken?.Type == L.COMMA) return null!; // 不准删逗号
106107
return base.SingleTokenDeletion(recognizer);
107108
}
108109

109110
private HashSet<int> insertionForbidden = [
110-
SimaiLexer.COMMA, SimaiLexer.KEY, SimaiLexer.SLIDE_TYPE, SimaiLexer.TOUCH_AREA, SimaiLexer.INT,
111-
SimaiLexer.CHART_END, SimaiLexer.FALSE_EACH,
112-
SimaiLexer.MODIFIER, SimaiLexer.NO_STAR, SimaiLexer.STAR_TO_TAP, SimaiLexer.TAP_TO_STAR
113-
]; // 逗号,和不确定的可能引起歧义的符号,一律不允许补充
111+
L.KEY, L.SLIDE_TYPE, L.TOUCH_AREA, L.INT, L.CHART_END, L.FALSE_EACH,
112+
L.MODIFIER, L.NO_STAR, L.STAR_TO_TAP, L.TAP_TO_STAR
113+
]; // 不确定的可能引起歧义的符号,一律不允许补充
114+
private HashSet<int> insertCommaOnlyWhen = [_literals["("], _literals["{"]];
114115

115116
protected override IToken GetMissingSymbol(Parser recognizer)
116117
{
117118
IToken currentToken = recognizer.CurrentToken;
118119

119120
// 不准插入insertionForbidden里提到的元素
120-
var insertionCandidates = GetExpectedTokens(recognizer).ToList().Where(x => !insertionForbidden.Contains(x)).ToList();
121+
var insertionCandidates = GetExpectedTokens(recognizer).ToList()
122+
.Where(x => !insertionForbidden.Contains(x)) // 上述黑名单中的不准插入
123+
.Where(x => x != L.COMMA || insertCommaOnlyWhen.Contains(recognizer.InputStream.LA(1))) // 逗号,只准在后面跟着的是'('或'{'的情况下才准插入
124+
.ToList();
121125
if (insertionCandidates.Count == 0) throw new InputMismatchException(recognizer); // 等价于SingleTokenInsertion返回false的情况,recoverInline失败、转交给上层recover处理
122126
int minElement = insertionCandidates[0];
123127

@@ -129,10 +133,21 @@ protected override IToken GetMissingSymbol(Parser recognizer)
129133
return this.ConstructToken(((ITokenStream) recognizer.InputStream).TokenSource, minElement, tokenText, current);
130134
}
131135

136+
protected override void ReportMissingToken(Parser recognizer)
137+
{
138+
try
139+
{ // 如果稍后GetMissingSymbol时将会被拒绝(抛异常),则跳过警告日志的打印,反正会去打印InputMismatch
140+
GetMissingSymbol(recognizer);
141+
base.ReportMissingToken(recognizer);
142+
}
143+
catch (InputMismatchException) {} // ignored
144+
}
145+
132146
private static Dictionary<string, int> _literals = Enumerable.Range(1, SimaiLexer.ruleNames.Length)
133-
.ToDictionary(i => SimaiLexer.DefaultVocabulary.GetLiteralName(i), i => i);
147+
.Where(i=>SimaiLexer.DefaultVocabulary.GetLiteralName(i) != null)
148+
.ToDictionary(i => SimaiLexer.DefaultVocabulary.GetLiteralName(i)[1..^1], i => i);
134149
private List<int> recoverySetAllowed = [
135-
SimaiLexer.COMMA, SimaiLexer.FALSE_EACH, _literals["'/'"], _literals["'('"], _literals["'{'"]
150+
L.COMMA, L.FALSE_EACH, _literals["/"], _literals["("], _literals["{"]
136151
]; // recover时,为了确保整个吞掉不合法的音符,而不是出现残缺的东西导致parser报错,只准同步到上面这些字符当中
137152

138153
public override void Recover(Parser recognizer, RecognitionException e)
@@ -155,7 +170,7 @@ public override void Recover(Parser recognizer, RecognitionException e)
155170
/**
156171
* 允许recoverInline,但是禁止大范围recover。
157172
*/
158-
public class ModerateErrorStrategy : LaxErrorStrategy
173+
public class ModerateErrorStrategy(SimaiParser simaiParser) : LaxErrorStrategy(simaiParser)
159174
{
160175
private BailErrorStrategy _bail = new();
161176

parser/simai/Simai.g4

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ FALSE_EACH: '`';
4747
eachSeparators: '/' | FALSE_EACH+;
4848
eachNote: eachSeparators note;
4949

50-
bpmTag: '(' number ')';
51-
absulouteStepTag: '{' '#' number '}';
52-
metTag: '{' int '}';
50+
bpmTag: (lp+='(')+ number (rp+=')')+;
51+
absulouteStepTag: (lp+='{')+ '#' number (rp+='}')+;
52+
metTag: (lp+='{')+ int (rp+='}')+;
5353

5454
note: slide (sharedHeadSlide)* | tap | KEY+ | hold | touch | touchHold; // tap+是因为,simai允许123这种语法、和1/2/3是等价的,但仅限tap之间。
5555

@@ -62,16 +62,16 @@ touch: TOUCH_AREA modifiers;
6262

6363
touchHold: TOUCH_AREA modifiers 'h' modifiers (duration modifiers)?;
6464

65-
duration: '[' (beats | '#' number) ']';
65+
duration: (lp+='[')+ (beats | '#' number) (rp+=']')+;
6666
beats: int ':' int;
6767

68-
slideDuration: '[' (
68+
slideDuration: (lp+='[')+ (
6969
beats
7070
| '#' number
7171
| waitTime '##' asBpm '#' (beats | number)
7272
| waitTime '##' (beats | number)
7373
| asBpm '#' (beats | number)
74-
) ']';
74+
) (rp+=']')+;
7575
waitTime: number;
7676
asBpm: number;
7777

parser/simai/SimaiParser.cs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Antlr4.Runtime.Tree;
44
using MuConvert.Antlr;
55
using MuConvert.chart;
6+
using MuConvert.parser.simai;
67
using MuConvert.utils;
78
using Rationals;
89
using static MuConvert.utils.Alert.LEVEL;
@@ -77,12 +78,14 @@ private string Preprocess(string text)
7778
try
7879
{ // 词语法分析
7980
var inputStream = new AntlrInputStream(text);
80-
var lexer = new SimaiLexer(inputStream) { ErrorListeners = { new ErrorListener(this) } };
81+
var lexer = new SimaiLexer(inputStream);
82+
lexer.RemoveErrorListeners();
83+
lexer.AddErrorListener(new ErrorListener(this));
8184
var tokens = new CommonTokenStream(lexer);
82-
var parser = new P(tokens) // MuConvert.Antlr.SimaiParser
83-
{
84-
ErrorHandler = ErrorStrategy(), ErrorListeners = { new ErrorListener(this) }
85-
};
85+
86+
var parser = new P(tokens) { ErrorHandler = ErrorStrategy() }; // MuConvert.Antlr.SimaiParser
87+
parser.RemoveErrorListeners();
88+
parser.AddErrorListener(new ErrorListener(this));
8689
root = parser.chart();
8790
}
8891
catch (Antlr4.Runtime.Misc.ParseCanceledException e)
@@ -116,10 +119,10 @@ private IAntlrErrorStrategy ErrorStrategy()
116119
case StrictLevelEnum.Strict:
117120
return new BailErrorStrategy();
118121
case StrictLevelEnum.Lax:
119-
return new LaxErrorStrategy();
122+
return new LaxErrorStrategy(this);
120123
case StrictLevelEnum.Normal:
121124
default:
122-
return new ModerateErrorStrategy();
125+
return new ModerateErrorStrategy(this);
123126
}
124127
}
125128

@@ -149,6 +152,16 @@ private void AddDefaultBpm()
149152
chart.BpmList.Add(new BPM(now, defaultStartBpm));
150153
}
151154

155+
private static bool SubtreeHasException(ParserRuleContext root)
156+
{
157+
if (root.exception != null) return true;
158+
foreach (var child in root.children ?? Array.Empty<IParseTree>())
159+
{
160+
if (child is ParserRuleContext pr && SubtreeHasException(pr)) return true;
161+
}
162+
return false;
163+
}
164+
152165
public sealed override object VisitNotations(P.NotationsContext context)
153166
{ // 形如 (120){4}1/1 算作一组notations
154167
foreach (var child in context.children ?? [])
@@ -178,14 +191,34 @@ public sealed override object VisitNotations(P.NotationsContext context)
178191
return true;
179192
}
180193

194+
private void WarnMoreParentheses(IList<IToken> ps)
195+
{
196+
if (ps.Count <= 1) return;
197+
var extraStr = "'" + string.Join("", ps.Skip(1).Select(x => x.Text)) + "'";
198+
if (StrictLevel == StrictLevelEnum.Strict)
199+
{ // 严格模式,抛异常
200+
AddAlert(Error, string.Format(Locale.RecoverInlineExtraneousTokenStrict, extraStr));
201+
throw new ConversionException(alerts);
202+
}
203+
else AddAlert(Warning, string.Format(Locale.RecoverInlineExtraneousToken, extraStr));
204+
}
205+
206+
private void WarnMoreParentheses(IList<IToken> lp, IList<IToken> rp)
207+
{
208+
WarnMoreParentheses(lp);
209+
WarnMoreParentheses(rp);
210+
}
211+
181212
public sealed override object VisitAbsulouteStepTag(P.AbsulouteStepTagContext context)
182213
{
183-
if (context.exception != null) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
214+
if (SubtreeHasException(context)) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
184215
if (!absoluteTimeStepWarned)
185216
{
186217
AddAlert(Warning, string.Format(Locale.AbsoluteStepUsed, context.GetText()), context);
187218
absoluteTimeStepWarned = true;
188219
}
220+
currContext = context;
221+
WarnMoreParentheses(context._lp, context._rp);
189222
absoluteTimeStep = (decimal)VisitNumber(context.number());
190223
var currentBpm = chart.BpmList.Last().Bpm;
191224
step = (Rational)absoluteTimeStep / (240 / (Rational)currentBpm);
@@ -194,8 +227,9 @@ public sealed override object VisitAbsulouteStepTag(P.AbsulouteStepTagContext co
194227

195228
public sealed override object VisitBpmTag(P.BpmTagContext context)
196229
{
197-
if (context.exception != null) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
230+
if (SubtreeHasException(context)) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
198231
currContext = context;
232+
WarnMoreParentheses(context._lp, context._rp);
199233
var bpm = (decimal)VisitNumber(context.number());
200234
chart.BpmList.Add(new BPM(now, bpm));
201235
if (absoluteTimeStep != null)
@@ -207,8 +241,9 @@ public sealed override object VisitBpmTag(P.BpmTagContext context)
207241

208242
public sealed override object VisitMetTag(P.MetTagContext context)
209243
{ // metTag指的是标记分音的tag,如{4}
210-
if (context.exception != null) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
244+
if (SubtreeHasException(context)) return false; // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
211245
currContext = context;
246+
WarnMoreParentheses(context._lp, context._rp);
212247
var quaver = int.Parse(context.@int().GetText());
213248
step = new Rational(1, quaver);
214249
absoluteTimeStep = null;
@@ -266,7 +301,7 @@ public sealed override object VisitNote(P.NoteContext context)
266301
// 注:这个函数返回的是List<Note>,因为ANTLR中的NoteContext,虽然大多数时候只对应一个Note,但有时也可能是两个以上!
267302
// 具体而言,两种情况:1. "1234"这种simai允许的tap多押简略记法(等价于"1/2/3/4")
268303
// 2. 同头星星如"1-2[2:1]*-3[2:1]",它在我们定义的ANTLR语法中是作为一个note节点的!
269-
if (context.exception != null) return new List<Note>(); // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
304+
if (SubtreeHasException(context)) return new List<Note>(); // 如果本节点下有异常,则直接整个吞掉,(避免具体的规则遇到不完整子树、爆出更不可预测的错误)
270305
currContext = context;
271306
List<Note> result = [];
272307
foreach (var child in context.children)
@@ -326,7 +361,7 @@ private void ApplyModifiers(P.ModifiersContext[] modifiersList, Note note, bool
326361
if (child is not ITerminalNode modifier) throw Utils.Fail("modifiers里面居然不是ITerminalNode");
327362
var token = modifier.Symbol;
328363
if (token.Text == "b" && note is not Touch) note.IsBreak = true;
329-
else if (token.Text == "x" && note is Tap) note.IsEx = false;
364+
else if (token.Text == "x" && note is Tap) note.IsEx = true;
330365
else if (token.Text == "f" && note is Touch touch) touch.IsFirework = true;
331366
else extraModifiers.Add(token);
332367
}
@@ -372,6 +407,7 @@ public sealed override object VisitDuration(P.DurationContext? context)
372407
result.InvariantBar = 0;
373408
return result;
374409
}
410+
WarnMoreParentheses(context._lp, context._rp);
375411
if (context.beats() != null) result.InvariantBar = (Rational)VisitBeats(context.beats());
376412
else result.Seconds = (Rational)(decimal)VisitNumber(context.number());
377413
return result;
@@ -417,6 +453,7 @@ public sealed override object VisitSlideDuration(P.SlideDurationContext context)
417453
var result = new Duration(currNote!);
418454
Duration? waitTime = null;
419455
isRealExactWaitTime = false;
456+
WarnMoreParentheses(context._lp, context._rp);
420457

421458
if (context.waitTime() != null)
422459
{

0 commit comments

Comments
 (0)