From 82af84a50dbd8b5a8b5124528673c43f300ec493 Mon Sep 17 00:00:00 2001 From: Diversus23 Date: Wed, 8 Apr 2026 14:00:26 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix=20#1468=20#6=20#1329:=20=D0=A0=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=20=D0=BE?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B9=D1=82=D0=B8=20/=20Goto=20=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=B5=20?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BA=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/CodeGeneratorPrivateTypes.cs | 18 + src/ScriptEngine/Compiler/CompilerErrors.cs | 12 + .../Compiler/StackMachineCodeGenerator.cs | 258 ++++++++++- .../GotoCodeGenerationTests.cs | 403 ++++++++++++++++++ tests/goto.os | 208 +++++++++ 5 files changed, 876 insertions(+), 23 deletions(-) create mode 100644 src/Tests/OneScript.Core.Tests/GotoCodeGenerationTests.cs create mode 100644 tests/goto.os diff --git a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs index 28b7b4af7..945e91a23 100644 --- a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs +++ b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs @@ -42,5 +42,23 @@ public static NestedLoopInfo New() public List breakStatements; public int tryNesting; } + + private class LabelInfo + { + public int codeIndex = DUMMY_ADDRESS; + public List<(string type, int id)> blockStack; + public int tryNesting; + } + + private struct PendingGoto + { + public int commandIndex; + public int exitTryIndex; + public string labelName; + public List<(string type, int id)> blockStack; + public List<(int commandIndex, string loopType, int blockId)> loopCleanupSlots; + public CodeRange location; + public int tryNesting; + } } } \ No newline at end of file diff --git a/src/ScriptEngine/Compiler/CompilerErrors.cs b/src/ScriptEngine/Compiler/CompilerErrors.cs index 9651e20ad..983ce5b99 100644 --- a/src/ScriptEngine/Compiler/CompilerErrors.cs +++ b/src/ScriptEngine/Compiler/CompilerErrors.cs @@ -29,6 +29,18 @@ public static CodeError MissedImport(string symbol, string libName) => Create($"Свойство {symbol} принадлежит пакету {libName}, который не импортирован в данном модуле", $"Property {symbol} belongs to package {libName} which is not imported in this module"); + public static CodeError DuplicateLabelDefinition(string name) => + Create($"Дублирование определения метки ~{name}", + $"Duplicate label definition ~{name}"); + + public static CodeError UndefinedLabel(string name) => + Create($"Метка не определена ~{name}", + $"Undefined label ~{name}"); + + public static CodeError InvalidGotoTarget(string name) => + Create($"На метку с указанным именем имеется недопустимый переход (~{name})", + $"Invalid goto target (~{name})"); + private static CodeError Create(string ru, string en, [CallerMemberName] string errorId = default) { return new CodeError diff --git a/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs b/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs index 3edae66b7..6d774216d 100644 --- a/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs +++ b/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs @@ -41,6 +41,20 @@ public partial class StackMachineCodeGenerator : BslSyntaxWalker private readonly List _forwardedMethods = new List(); private readonly Stack _nestedLoops = new Stack(); + private readonly Dictionary _labels = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly List _pendingGotos = new List(); + private readonly List<(string type, int id)> _blockStack = new List<(string type, int id)>(); + private int _tryNestingCount; + private int _blockIdCounter; + + private const string BlockWhile = "while"; + private const string BlockForEach = "foreach"; + private const string BlockFor = "for"; + private const string BlockIf = "if"; + private const string BlockElseIf = "elseif"; + private const string BlockElse = "else"; + private const string BlockTry = "try"; + private const string BlockExcept = "except"; private IBslProcess _compilerProcess; @@ -197,6 +211,7 @@ protected override void VisitModuleBody(BslSyntaxNode child) if (child.Children.Count == 0) return; + ResetLabelState(); var entry = _module.Code.Count; var localCtx = new SymbolScope(); _ctx.PushScope(localCtx, ScopeBindingDescriptor.ThisScope()); @@ -211,6 +226,7 @@ protected override void VisitModuleBody(BslSyntaxNode child) throw; } + FinalizePendingGotos(); _ctx.PopScope(); var topIdx = _ctx.ScopeCount - 1; @@ -249,12 +265,118 @@ private static string[] GetVariableNames(SymbolScope localCtx) protected override void VisitGotoNode(NonTerminalNode node) { - throw new NotSupportedException(); + var labelNode = (LabelNode)node.Children[0]; + var labelName = labelNode.LabelName; + + if (_labels.TryGetValue(labelName, out var labelInfo) && labelInfo.codeIndex != DUMMY_ADDRESS) + { + // Backward goto — label already known + var currentStack = SnapshotBlockStack(); + if (!IsValidGotoTarget(currentStack, labelInfo.blockStack)) + { + AddError(CompilerErrors.InvalidGotoTarget(labelName), node.Location); + return; + } + + GenerateLoopCleanup(currentStack, labelInfo.blockStack); + + var tryDiff = _tryNestingCount - labelInfo.tryNesting; + if (tryDiff > 0) + AddCommand(OperationCode.ExitTry, tryDiff); + + AddCommand(OperationCode.Jmp, labelInfo.codeIndex); + } + else + { + // Forward goto — label not yet known + if (!_labels.ContainsKey(labelName)) + _labels[labelName] = new LabelInfo(); + + var currentStack = SnapshotBlockStack(); + + // Reserve cleanup slots for loops (innermost to outermost) + var cleanupSlots = new List<(int commandIndex, string loopType, int blockId)>(); + for (int i = currentStack.Count - 1; i >= 0; i--) + { + var block = currentStack[i]; + if (block.type == BlockForEach || block.type == BlockFor) + { + var idx = AddCommand(OperationCode.Nop); + cleanupSlots.Add((idx, block.type, block.id)); + } + } + + var exitTryIndex = _tryNestingCount > 0 + ? AddCommand(OperationCode.ExitTry, 0) + : -1; + var jmpIndex = AddCommand(OperationCode.Jmp, DUMMY_ADDRESS); + + _pendingGotos.Add(new PendingGoto + { + commandIndex = jmpIndex, + exitTryIndex = exitTryIndex, + labelName = labelName, + blockStack = currentStack, + loopCleanupSlots = cleanupSlots, + location = node.Location, + tryNesting = _tryNestingCount + }); + } } protected override void VisitLabelNode(LabelNode node) { - throw new NotSupportedException(); + var labelName = node.LabelName; + + if (_labels.TryGetValue(labelName, out var existing) && existing.codeIndex != DUMMY_ADDRESS) + { + AddError(CompilerErrors.DuplicateLabelDefinition(labelName), node.Location); + return; + } + + var labelInfo = existing ?? new LabelInfo(); + labelInfo.codeIndex = _module.Code.Count; + labelInfo.blockStack = SnapshotBlockStack(); + labelInfo.tryNesting = _tryNestingCount; + _labels[labelName] = labelInfo; + + // Resolve pending forward gotos targeting this label + for (int i = _pendingGotos.Count - 1; i >= 0; i--) + { + var pending = _pendingGotos[i]; + if (!string.Equals(pending.labelName, labelName, StringComparison.OrdinalIgnoreCase)) + continue; + + if (!IsValidGotoTarget(pending.blockStack, labelInfo.blockStack)) + { + AddError(CompilerErrors.InvalidGotoTarget(labelName), pending.location); + } + else + { + // Patch loop cleanup slots + var exitedBlockIds = new HashSet(); + for (int j = labelInfo.blockStack.Count; j < pending.blockStack.Count; j++) + exitedBlockIds.Add(pending.blockStack[j].id); + + foreach (var slot in pending.loopCleanupSlots) + { + if (exitedBlockIds.Contains(slot.blockId)) + { + if (slot.loopType == BlockForEach) + CorrectCommand(slot.commandIndex, OperationCode.StopIterator, 0); + else if (slot.loopType == BlockFor) + CorrectCommand(slot.commandIndex, OperationCode.PopTmp, 1); + } + } + + var tryDiff = pending.tryNesting - labelInfo.tryNesting; + if (tryDiff > 0 && pending.exitTryIndex != -1) + CorrectCommandArgument(pending.exitTryIndex, tryDiff); + CorrectCommandArgument(pending.commandIndex, labelInfo.codeIndex); + } + + _pendingGotos.RemoveAt(i); + } } protected override void VisitMethod(MethodNode methodNode) @@ -332,23 +454,25 @@ protected override void VisitMethod(MethodNode methodNode) protected override void VisitMethodBody(MethodNode methodNode) { + ResetLabelState(); var codeStart = _module.Code.Count; - + foreach (var variableDefinition in methodNode.VariableDefinitions()) { VisitMethodVariable(methodNode, variableDefinition); } VisitCodeBlock(methodNode.MethodBody); - + if (methodNode.Signature.IsFunction) { // неявный возврат Undefined AddCommand(OperationCode.PushUndef); } - + var codeEnd = _module.Code.Count; - + FinalizePendingGotos(); + VisitBlockEnd(methodNode.EndLocation); // debug last line num AddCommand(OperationCode.Return); @@ -383,15 +507,17 @@ protected override void VisitWhileNode(WhileLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = conditionIndex; _nestedLoops.Push(loopRecord); + PushBlock(BlockWhile); base.VisitExpression(node.Children[0]); var jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); - + VisitCodeBlock(node.Children[1]); VisitBlockEnd(node.EndLocation); - + AddCommand(OperationCode.Jmp, conditionIndex); var endLoop = AddCommand(OperationCode.Nop); CorrectCommandArgument(jumpFalseIndex, endLoop); + PopBlock(); CorrectBreakStatements(_nestedLoops.Pop(), endLoop); } @@ -409,14 +535,16 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = loopBegin; _nestedLoops.Push(loopRecord); - + PushBlock(BlockForEach); + VisitIteratorLoopBody(node.LoopBody); VisitBlockEnd(node.EndLocation); - + AddCommand(OperationCode.Jmp, loopBegin); - + var indexLoopEnd = AddCommand(OperationCode.StopIterator); CorrectCommandArgument(condition, indexLoopEnd); + PopBlock(); CorrectBreakStatements(_nestedLoops.Pop(), indexLoopEnd); } @@ -447,6 +575,7 @@ protected override void VisitForLoopNode(ForLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = indexLoopBegin; _nestedLoops.Push(loopRecord); + PushBlock(BlockFor); VisitCodeBlock(node.LoopBody); VisitBlockEnd(node.EndLocation); @@ -456,6 +585,7 @@ protected override void VisitForLoopNode(ForLoopNode node) var indexLoopEnd = AddCommand(OperationCode.PopTmp, 1); CorrectCommandArgument(conditionIndex, indexLoopEnd); + PopBlock(); CorrectBreakStatements(_nestedLoops.Pop(), indexLoopEnd); } @@ -504,11 +634,13 @@ protected override void VisitIfNode(ConditionNode node) var jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); + PushBlock(BlockIf); VisitIfTruePart(node.TruePart); + PopBlock(); exitIndices.Add(AddCommand(OperationCode.Jmp, DUMMY_ADDRESS)); bool hasAlternativeBranches = false; - + foreach (var alternative in node.GetAlternatives()) { CorrectCommandArgument(jumpFalseIndex, _module.Code.Count); @@ -517,7 +649,9 @@ protected override void VisitIfNode(ConditionNode node) AddLineNumber(alternative.Location.LineNumber); VisitIfExpression(elif.Expression); jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); + PushBlock(BlockElseIf); VisitIfTruePart(elif.TruePart); + PopBlock(); exitIndices.Add(AddCommand(OperationCode.Jmp, DUMMY_ADDRESS)); } else @@ -525,7 +659,9 @@ protected override void VisitIfNode(ConditionNode node) hasAlternativeBranches = true; CorrectCommandArgument(jumpFalseIndex, _module.Code.Count); AddLineNumber(alternative.Location.LineNumber, CodeGenerationFlags.CodeStatistics); + PushBlock(BlockElse); VisitCodeBlock(alternative); + PopBlock(); } } @@ -850,17 +986,17 @@ private void GlobalCall(CallNode call, bool asFunction) if (asFunction) AddCommand(OperationCode.CallFunc, GetMethodRefNumber(methBinding)); else - AddCommand(OperationCode.CallProc, GetMethodRefNumber(methBinding)); + AddCommand(OperationCode.CallProc, GetMethodRefNumber(methBinding)); } else { // can be defined later - var forwarded = new ForwardedMethodDecl - { - identifier = identifier, - asFunction = asFunction, - location = identifierNode.Location, - factArguments = argList + var forwarded = new ForwardedMethodDecl + { + identifier = identifier, + asFunction = asFunction, + location = identifierNode.Location, + factArguments = argList }; PushCallArguments(call.ArgumentList); @@ -965,10 +1101,19 @@ protected override void VisitTryExceptNode(TryExceptNode node) protected override void VisitTryBlock(CodeBatchNode node) { PushTryNesting(); + PushBlock(BlockTry); base.VisitTryBlock(node); + PopBlock(); PopTryNesting(); } + protected override void VisitExceptBlock(CodeBatchNode node) + { + PushBlock(BlockExcept); + base.VisitExceptBlock(node); + PopBlock(); + } + protected override void VisitExecuteStatement(BslSyntaxNode node) { base.VisitExecuteStatement(node); @@ -1066,8 +1211,8 @@ private void MakeNewObjectStatic(NewObjectNode node) { PushCallArguments(node.ConstructorArguments); } - else - { + else + { AddCommand(OperationCode.ArgNum, 0); } @@ -1097,7 +1242,74 @@ private void PopTryNesting() _nestedLoops.Peek().tryNesting--; } } - + + private void PushBlock(string blockType) + { + _blockStack.Add((blockType, _blockIdCounter++)); + if (blockType == BlockTry) + _tryNestingCount++; + } + + private void PopBlock() + { + var last = _blockStack[_blockStack.Count - 1]; + _blockStack.RemoveAt(_blockStack.Count - 1); + if (last.type == BlockTry) + _tryNestingCount--; + } + + private List<(string type, int id)> SnapshotBlockStack() + { + return new List<(string type, int id)>(_blockStack); + } + + private static bool IsValidGotoTarget(List<(string type, int id)> gotoStack, List<(string type, int id)> labelStack) + { + if (labelStack.Count > gotoStack.Count) + return false; + for (int i = 0; i < labelStack.Count; i++) + { + if (labelStack[i].type != gotoStack[i].type || labelStack[i].id != gotoStack[i].id) + return false; + } + return true; + } + + private void ResetLabelState() + { + _labels.Clear(); + _pendingGotos.Clear(); + _blockStack.Clear(); + _tryNestingCount = 0; + _blockIdCounter = 0; + } + + private void GenerateLoopCleanup(List<(string type, int id)> gotoStack, List<(string type, int id)> labelStack) + { + // Generate cleanup from innermost to outermost for exited loops + for (int i = gotoStack.Count - 1; i >= labelStack.Count; i--) + { + if (gotoStack[i].type == BlockForEach) + AddCommand(OperationCode.StopIterator); + else if (gotoStack[i].type == BlockFor) + AddCommand(OperationCode.PopTmp, 1); + } + } + + private void CorrectCommand(int index, OperationCode code, int argument) + { + _module.Code[index] = new Command { Code = code, Argument = argument }; + } + + private void FinalizePendingGotos() + { + foreach (var pending in _pendingGotos) + { + AddError(CompilerErrors.UndefinedLabel(pending.labelName), pending.location); + } + _pendingGotos.Clear(); + } + private void CorrectCommandArgument(int index, int newArgument) { var cmd = _module.Code[index]; @@ -1319,7 +1531,7 @@ private int GetConstNumber(in ConstDefinition cDef) } private int GetIdentNumber(string ident) - { + { var idx = _module.Identifiers.IndexOf(ident); if (idx < 0) { diff --git a/src/Tests/OneScript.Core.Tests/GotoCodeGenerationTests.cs b/src/Tests/OneScript.Core.Tests/GotoCodeGenerationTests.cs new file mode 100644 index 000000000..f61c1eaad --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/GotoCodeGenerationTests.cs @@ -0,0 +1,403 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Moq; +using OneScript.Compilation.Binding; +using OneScript.Execution; +using OneScript.Language; +using OneScript.Language.LexicalAnalysis; +using OneScript.Language.SyntaxAnalysis; +using OneScript.Language.SyntaxAnalysis.AstNodes; +using OneScript.Sources; +using ScriptEngine; +using ScriptEngine.Compiler; +using ScriptEngine.Machine; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class GotoCodeGenerationTests + { + private static StackRuntimeModule BuildModule(string code) + { + var lexer = new DefaultLexer(); + lexer.Iterator = SourceCodeBuilder.Create().FromString(code).Build().CreateIterator(); + var errSink = new ThrowingErrorSink(); + var parser = new DefaultBslParser( + lexer, + errSink, + Mock.Of()); + + var node = parser.ParseStatefulModule() as ModuleNode; + var ctx = new SymbolTable(); + ctx.PushScope(new SymbolScope(), ScopeBindingDescriptor.Static(null)); + var compiler = new StackMachineCodeGenerator(errSink, ExplicitImportsBehavior.Disabled); + return compiler.CreateModule(node, lexer.Iterator.Source, ctx, Mock.Of()); + } + + private static StackRuntimeModule BuildModuleWithErrors(string code, out List errors) + { + var lexer = new DefaultLexer(); + lexer.Iterator = SourceCodeBuilder.Create().FromString(code).Build().CreateIterator(); + var errSink = new ListErrorSink(); + var parser = new DefaultBslParser( + lexer, + errSink, + Mock.Of()); + + var node = parser.ParseStatefulModule() as ModuleNode; + var ctx = new SymbolTable(); + ctx.PushScope(new SymbolScope(), ScopeBindingDescriptor.Static(null)); + var compiler = new StackMachineCodeGenerator(errSink, ExplicitImportsBehavior.Disabled); + var module = compiler.CreateModule(node, lexer.Iterator.Source, ctx, Mock.Of()); + errors = new List(errSink.Errors); + return module; + } + + [Fact] + public void Forward_Goto_Compiles_Successfully() + { + var code = @" + А = 1; + Перейти ~Метка; + А = 2; + ~Метка: + А = 3;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + module.Code.Should().Contain(c => c.Code == OperationCode.Jmp); + } + + [Fact] + public void Backward_Goto_Compiles_Successfully() + { + var code = @" + А = 0; + ~Начало: + А = А + 1; + Если А < 5 Тогда + Перейти ~Начало; + КонецЕсли;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + module.Code.Should().Contain(c => c.Code == OperationCode.Jmp); + } + + [Fact] + public void Goto_Out_Of_Loop_Compiles_Successfully() + { + var code = @" + Для Инд = 1 По 10 Цикл + Для Инд2 = 1 По 10 Цикл + Если Инд2 = 5 Тогда + Перейти ~ВыходИзЦиклов; + КонецЕсли; + КонецЦикла; + КонецЦикла; + ~ВыходИзЦиклов: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + } + + [Fact] + public void Goto_Out_Of_If_Compiles_Successfully() + { + var code = @" + А = 1; + Если А = 1 Тогда + Перейти ~ПослеУсловия; + А = 2; + КонецЕсли; + ~ПослеУсловия: + А = 3;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + } + + [Fact] + public void Goto_Out_Of_Try_Compiles_With_ExitTry() + { + var code = @" + Попытка + Перейти ~ПослеПопытки; + Исключение + КонецПопытки; + ~ПослеПопытки: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + module.Code.Should().Contain(c => c.Code == OperationCode.ExitTry); + } + + [Fact] + public void Goto_In_Procedure_Compiles_Successfully() + { + var code = @" + Процедура Тест() + Перейти ~Конец; + А = 1; + ~Конец: + КонецПроцедуры"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + } + + [Fact] + public void Goto_Into_Loop_Is_Error() + { + var code = @" + Перейти ~Внутри; + Для Инд = 1 По 10 Цикл + ~Внутри: + А = 1; + КонецЦикла;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Goto_Into_If_Is_Error() + { + var code = @" + Перейти ~Внутри; + Если Истина Тогда + ~Внутри: + А = 1; + КонецЕсли;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Goto_Into_Try_Is_Error() + { + var code = @" + Перейти ~Внутри; + Попытка + ~Внутри: + А = 1; + Исключение + КонецПопытки;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Goto_Into_Except_Is_Error() + { + var code = @" + Перейти ~Внутри; + Попытка + Исключение + ~Внутри: + А = 1; + КонецПопытки;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Undefined_Label_Is_Error() + { + var code = @" + Перейти ~НесуществующаяМетка; + А = 1;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.UndefinedLabel)); + } + + [Fact] + public void Duplicate_Label_Is_Error() + { + var code = @" + ~Метка: + А = 1; + ~Метка: + А = 2;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.DuplicateLabelDefinition)); + } + + [Fact] + public void Goto_Between_Sibling_Blocks_Is_Error() + { + var code = @" + Для Инд = 1 По 10 Цикл + Перейти ~Цель; + КонецЦикла; + Пока Истина Цикл + ~Цель: + Прервать; + КонецЦикла;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Labels_Do_Not_Leak_Between_Methods() + { + var code = @" + Процедура Первая() + ~Метка: + А = 1; + КонецПроцедуры + + Процедура Вторая() + Перейти ~Метка; + КонецПроцедуры"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.UndefinedLabel)); + } + + [Fact] + public void Goto_Out_Of_Except_Compiles_Successfully() + { + var code = @" + Попытка + А = 1 / 0; + Исключение + Перейти ~ПослеОбработки; + КонецПопытки; + ~ПослеОбработки: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + } + + [Fact] + public void Goto_From_Except_To_Try_Of_Same_Block_Is_Error() + { + var code = @" + Попытка + ~Внутри: + А = 1; + Исключение + Перейти ~Внутри; + КонецПопытки;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Goto_Out_Of_Nested_Try_Generates_ExitTry_With_Correct_Depth() + { + var code = @" + Попытка + Попытка + Перейти ~Снаружи; + Исключение + КонецПопытки; + Исключение + КонецПопытки; + ~Снаружи: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + // должен быть ExitTry с аргументом 2 (выход из двух вложенных try) + module.Code.Should().Contain(c => c.Code == OperationCode.ExitTry && c.Argument == 2); + } + + [Fact] + public void Forward_Goto_Outside_Try_Does_Not_Generate_ExitTry() + { + var code = @" + А = 1; + Перейти ~Метка; + А = 2; + ~Метка: + А = 3;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + // ExitTry не должен генерироваться — goto вне try-блока + module.Code.Should().NotContain(c => c.Code == OperationCode.ExitTry); + } + + [Fact] + public void Goto_Out_Of_ForEach_Generates_StopIterator() + { + var code = @" + Массив = Новый Массив; + Для Каждого Элемент Из Массив Цикл + Перейти ~ПослеЦикла; + КонецЦикла; + ~ПослеЦикла: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + // goto из ForEach должен генерировать StopIterator для очистки итератора + module.Code.Should().Contain(c => c.Code == OperationCode.StopIterator); + } + + [Fact] + public void Goto_Out_Of_For_Generates_PopTmp() + { + var code = @" + Для Инд = 1 По 10 Цикл + Перейти ~ПослеЦикла; + КонецЦикла; + ~ПослеЦикла: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + // goto из Для должен генерировать PopTmp для очистки верхней границы + module.Code.Where(c => c.Code == OperationCode.PopTmp).Should().HaveCountGreaterThan(1); + } + + [Fact] + public void Goto_Between_Same_Type_Sibling_Blocks_Is_Error() + { + var code = @" + Если Истина Тогда + Перейти ~Цель; + КонецЕсли; + Если Истина Тогда + ~Цель: + А = 1; + КонецЕсли;"; + + BuildModuleWithErrors(code, out var errors); + errors.Should().Contain(e => e.ErrorId == nameof(CompilerErrors.InvalidGotoTarget)); + } + + [Fact] + public void Case_Insensitive_Labels_Work() + { + var code = @" + Перейти ~метка; + А = 999; + ~Метка: + А = 1;"; + + var module = BuildModule(code); + module.Should().NotBeNull(); + } + } +} diff --git a/tests/goto.os b/tests/goto.os new file mode 100644 index 000000000..80791b3e9 --- /dev/null +++ b/tests/goto.os @@ -0,0 +1,208 @@ +/////////////////////////////////////////////////////////////////////// +// +// Тест оператора Перейти (Goto) +// +/////////////////////////////////////////////////////////////////////// + +Перем юТест; + +//////////////////////////////////////////////////////////////////// +// Программный интерфейс + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходВперед"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходНазад"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьВыходИзВложенныхЦиклов"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьВыходИзУсловия"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьВыходИзПопытки"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходВПроцедуре"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьНесколькоМетокВМодуле"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПереходВФункции"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЭмуляциюЦиклаЧерезGoto"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьЭмуляциюRepeatUntil"); + + Возврат ВсеТесты; + +КонецФункции + +Процедура ТестДолжен_ПроверитьПереходВперед() Экспорт + + А = 1; + Перейти ~Метка; + А = 999; + ~Метка: + юТест.ПроверитьРавенство(А, 1, "Переход вперед: код между goto и меткой не должен выполняться"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПереходНазад() Экспорт + + Счетчик = 0; + ~Начало: + Счетчик = Счетчик + 1; + Если Счетчик < 5 Тогда + Перейти ~Начало; + КонецЕсли; + + юТест.ПроверитьРавенство(Счетчик, 5, "Переход назад: цикл через goto должен отработать 5 раз"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьВыходИзВложенныхЦиклов() Экспорт + + Результат = 0; + Для Индекс1 = 1 По 10 Цикл + Для Индекс2 = 1 По 10 Цикл + Результат = Результат + 1; + Если Индекс2 = 3 Тогда + Перейти ~ВыходИзЦиклов; + КонецЕсли; + КонецЦикла; + КонецЦикла; + ~ВыходИзЦиклов: + + юТест.ПроверитьРавенство(Результат, 3, "Выход из вложенных циклов: должно быть 3 итерации внутреннего цикла"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьВыходИзУсловия() Экспорт + + А = 1; + Б = 0; + Если А = 1 Тогда + Б = 10; + Перейти ~ПослеУсловия; + Б = 999; + КонецЕсли; + ~ПослеУсловия: + + юТест.ПроверитьРавенство(Б, 10, "Выход из условия: код после goto не должен выполняться"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьВыходИзПопытки() Экспорт + + А = 0; + Попытка + А = 1; + Перейти ~ПослеПопытки; + А = 999; + Исключение + А = -1; + КонецПопытки; + ~ПослеПопытки: + + юТест.ПроверитьРавенство(А, 1, "Выход из попытки: должно быть значение до goto"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПереходВПроцедуре() Экспорт + + Результат = ВспомогательнаяПроцедураСGoto(); + юТест.ПроверитьРавенство(Результат, 42, "Переход в процедуре: goto внутри функции"); + +КонецПроцедуры + +Функция ВспомогательнаяПроцедураСGoto() + А = 42; + Перейти ~Конец; + А = 0; + ~Конец: + Возврат А; +КонецФункции + +Процедура ТестДолжен_ПроверитьНесколькоМетокВМодуле() Экспорт + + А = 0; + Б = 0; + + Перейти ~Первая; + А = 999; + ~Первая: + А = 1; + + Перейти ~Вторая; + Б = 999; + ~Вторая: + Б = 2; + + юТест.ПроверитьРавенство(А, 1, "Несколько меток: первая метка"); + юТест.ПроверитьРавенство(Б, 2, "Несколько меток: вторая метка"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПереходВФункции() Экспорт + + Результат = ФункцияСНесколькимиПереходами(3); + юТест.ПроверитьРавенство(Результат, "три", "Переход в функции с несколькими метками"); + + Результат = ФункцияСНесколькимиПереходами(1); + юТест.ПроверитьРавенство(Результат, "один", "Переход в функции с несколькими метками"); + + Результат = ФункцияСНесколькимиПереходами(99); + юТест.ПроверитьРавенство(Результат, "другое", "Переход в функции с несколькими метками"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЭмуляциюЦиклаЧерезGoto() Экспорт + + // Эмуляция цикла Для Сч = 1 По 10 через Goto + Сумма = 0; + Сч = 1; + ~НачалоЦикла: + Если Сч > 10 Тогда + Перейти ~ВыходЦикла; + КонецЕсли; + Сумма = Сумма + Сч; + Сч = Сч + 1; + Перейти ~НачалоЦикла; + ~ВыходЦикла: + + юТест.ПроверитьРавенство(Сумма, 55, "Эмуляция цикла: сумма чисел от 1 до 10 должна быть 55"); + юТест.ПроверитьРавенство(Сч, 11, "Эмуляция цикла: счетчик должен быть 11 после выхода"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьЭмуляциюRepeatUntil() Экспорт + + // Эмуляция repeat...until: тело выполняется минимум 1 раз, + // условие выхода проверяется в конце + Сч = 0; + Произведение = 1; + ~ТелоЦикла: + Сч = Сч + 1; + Произведение = Произведение * Сч; + // until Сч >= 5 + Если Сч < 5 Тогда + Перейти ~ТелоЦикла; + КонецЕсли; + + // 1*2*3*4*5 = 120 + юТест.ПроверитьРавенство(Произведение, 120, "Repeat/Until: факториал 5 должен быть 120"); + юТест.ПроверитьРавенство(Сч, 5, "Repeat/Until: счетчик должен быть 5"); + +КонецПроцедуры + +Функция ФункцияСНесколькимиПереходами(Знач Номер) + Если Номер = 1 Тогда + Перейти ~Метка1; + ИначеЕсли Номер = 3 Тогда + Перейти ~Метка3; + Иначе + Перейти ~МеткаДругое; + КонецЕсли; + + ~Метка1: + Возврат "один"; + + ~Метка3: + Возврат "три"; + + ~МеткаДругое: + Возврат "другое"; +КонецФункции From 4f4ebffd97a7faa5806abf63b5396901f7bf8887 Mon Sep 17 00:00:00 2001 From: Diversus23 Date: Thu, 9 Apr 2026 13:49:17 +0300 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=D0=A0=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20Goto=20\=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B9=D1=82=D0=B8=20=D0=BF=D0=BE=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=87=D0=B0=D0=BD=D0=B8=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/CodeGeneratorPrivateTypes.cs | 18 ++++- .../Compiler/StackMachineCodeGenerator.cs | 65 ++++++++----------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs index 945e91a23..69522b883 100644 --- a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs +++ b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs @@ -43,10 +43,22 @@ public static NestedLoopInfo New() public int tryNesting; } + private enum BlockType + { + While, + ForEach, + For, + If, + ElseIf, + Else, + Try, + Except + } + private class LabelInfo { public int codeIndex = DUMMY_ADDRESS; - public List<(string type, int id)> blockStack; + public List<(BlockType type, int id)> blockStack; public int tryNesting; } @@ -55,8 +67,8 @@ private struct PendingGoto public int commandIndex; public int exitTryIndex; public string labelName; - public List<(string type, int id)> blockStack; - public List<(int commandIndex, string loopType, int blockId)> loopCleanupSlots; + public List<(BlockType type, int id)> blockStack; + public List<(int commandIndex, BlockType loopType, int blockId)> loopCleanupSlots; public CodeRange location; public int tryNesting; } diff --git a/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs b/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs index 6d774216d..05c77600e 100644 --- a/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs +++ b/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs @@ -43,19 +43,10 @@ public partial class StackMachineCodeGenerator : BslSyntaxWalker private readonly Stack _nestedLoops = new Stack(); private readonly Dictionary _labels = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly List _pendingGotos = new List(); - private readonly List<(string type, int id)> _blockStack = new List<(string type, int id)>(); + private readonly List<(BlockType type, int id)> _blockStack = new List<(BlockType type, int id)>(); private int _tryNestingCount; private int _blockIdCounter; - private const string BlockWhile = "while"; - private const string BlockForEach = "foreach"; - private const string BlockFor = "for"; - private const string BlockIf = "if"; - private const string BlockElseIf = "elseif"; - private const string BlockElse = "else"; - private const string BlockTry = "try"; - private const string BlockExcept = "except"; - private IBslProcess _compilerProcess; private HashSet _reportedOldProperties = new HashSet(); @@ -270,7 +261,7 @@ protected override void VisitGotoNode(NonTerminalNode node) if (_labels.TryGetValue(labelName, out var labelInfo) && labelInfo.codeIndex != DUMMY_ADDRESS) { - // Backward goto — label already known + // Обратный переход, метка уже определена var currentStack = SnapshotBlockStack(); if (!IsValidGotoTarget(currentStack, labelInfo.blockStack)) { @@ -288,18 +279,18 @@ protected override void VisitGotoNode(NonTerminalNode node) } else { - // Forward goto — label not yet known + // Прямой переход, метка еще не определена if (!_labels.ContainsKey(labelName)) _labels[labelName] = new LabelInfo(); var currentStack = SnapshotBlockStack(); - // Reserve cleanup slots for loops (innermost to outermost) - var cleanupSlots = new List<(int commandIndex, string loopType, int blockId)>(); + // Резервируем слоты очистки для циклов (от внутреннего к внешнему) + var cleanupSlots = new List<(int commandIndex, BlockType loopType, int blockId)>(); for (int i = currentStack.Count - 1; i >= 0; i--) { var block = currentStack[i]; - if (block.type == BlockForEach || block.type == BlockFor) + if (block.type == BlockType.ForEach || block.type == BlockType.For) { var idx = AddCommand(OperationCode.Nop); cleanupSlots.Add((idx, block.type, block.id)); @@ -340,7 +331,7 @@ protected override void VisitLabelNode(LabelNode node) labelInfo.tryNesting = _tryNestingCount; _labels[labelName] = labelInfo; - // Resolve pending forward gotos targeting this label + // Разрешаем отложенные прямые переходы, указывающие на эту метку for (int i = _pendingGotos.Count - 1; i >= 0; i--) { var pending = _pendingGotos[i]; @@ -353,7 +344,7 @@ protected override void VisitLabelNode(LabelNode node) } else { - // Patch loop cleanup slots + // Заполняем слоты очистки циклов var exitedBlockIds = new HashSet(); for (int j = labelInfo.blockStack.Count; j < pending.blockStack.Count; j++) exitedBlockIds.Add(pending.blockStack[j].id); @@ -362,9 +353,9 @@ protected override void VisitLabelNode(LabelNode node) { if (exitedBlockIds.Contains(slot.blockId)) { - if (slot.loopType == BlockForEach) + if (slot.loopType == BlockType.ForEach) CorrectCommand(slot.commandIndex, OperationCode.StopIterator, 0); - else if (slot.loopType == BlockFor) + else if (slot.loopType == BlockType.For) CorrectCommand(slot.commandIndex, OperationCode.PopTmp, 1); } } @@ -507,7 +498,7 @@ protected override void VisitWhileNode(WhileLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = conditionIndex; _nestedLoops.Push(loopRecord); - PushBlock(BlockWhile); + PushBlock(BlockType.While); base.VisitExpression(node.Children[0]); var jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); @@ -535,7 +526,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = loopBegin; _nestedLoops.Push(loopRecord); - PushBlock(BlockForEach); + PushBlock(BlockType.ForEach); VisitIteratorLoopBody(node.LoopBody); VisitBlockEnd(node.EndLocation); @@ -575,7 +566,7 @@ protected override void VisitForLoopNode(ForLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = indexLoopBegin; _nestedLoops.Push(loopRecord); - PushBlock(BlockFor); + PushBlock(BlockType.For); VisitCodeBlock(node.LoopBody); VisitBlockEnd(node.EndLocation); @@ -634,7 +625,7 @@ protected override void VisitIfNode(ConditionNode node) var jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); - PushBlock(BlockIf); + PushBlock(BlockType.If); VisitIfTruePart(node.TruePart); PopBlock(); exitIndices.Add(AddCommand(OperationCode.Jmp, DUMMY_ADDRESS)); @@ -649,7 +640,7 @@ protected override void VisitIfNode(ConditionNode node) AddLineNumber(alternative.Location.LineNumber); VisitIfExpression(elif.Expression); jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); - PushBlock(BlockElseIf); + PushBlock(BlockType.ElseIf); VisitIfTruePart(elif.TruePart); PopBlock(); exitIndices.Add(AddCommand(OperationCode.Jmp, DUMMY_ADDRESS)); @@ -659,7 +650,7 @@ protected override void VisitIfNode(ConditionNode node) hasAlternativeBranches = true; CorrectCommandArgument(jumpFalseIndex, _module.Code.Count); AddLineNumber(alternative.Location.LineNumber, CodeGenerationFlags.CodeStatistics); - PushBlock(BlockElse); + PushBlock(BlockType.Else); VisitCodeBlock(alternative); PopBlock(); } @@ -1101,7 +1092,7 @@ protected override void VisitTryExceptNode(TryExceptNode node) protected override void VisitTryBlock(CodeBatchNode node) { PushTryNesting(); - PushBlock(BlockTry); + PushBlock(BlockType.Try); base.VisitTryBlock(node); PopBlock(); PopTryNesting(); @@ -1109,7 +1100,7 @@ protected override void VisitTryBlock(CodeBatchNode node) protected override void VisitExceptBlock(CodeBatchNode node) { - PushBlock(BlockExcept); + PushBlock(BlockType.Except); base.VisitExceptBlock(node); PopBlock(); } @@ -1243,10 +1234,10 @@ private void PopTryNesting() } } - private void PushBlock(string blockType) + private void PushBlock(BlockType blockType) { _blockStack.Add((blockType, _blockIdCounter++)); - if (blockType == BlockTry) + if (blockType == BlockType.Try) _tryNestingCount++; } @@ -1254,16 +1245,16 @@ private void PopBlock() { var last = _blockStack[_blockStack.Count - 1]; _blockStack.RemoveAt(_blockStack.Count - 1); - if (last.type == BlockTry) + if (last.type == BlockType.Try) _tryNestingCount--; } - private List<(string type, int id)> SnapshotBlockStack() + private List<(BlockType type, int id)> SnapshotBlockStack() { - return new List<(string type, int id)>(_blockStack); + return new List<(BlockType type, int id)>(_blockStack); } - private static bool IsValidGotoTarget(List<(string type, int id)> gotoStack, List<(string type, int id)> labelStack) + private static bool IsValidGotoTarget(List<(BlockType type, int id)> gotoStack, List<(BlockType type, int id)> labelStack) { if (labelStack.Count > gotoStack.Count) return false; @@ -1284,14 +1275,14 @@ private void ResetLabelState() _blockIdCounter = 0; } - private void GenerateLoopCleanup(List<(string type, int id)> gotoStack, List<(string type, int id)> labelStack) + private void GenerateLoopCleanup(List<(BlockType type, int id)> gotoStack, List<(BlockType type, int id)> labelStack) { - // Generate cleanup from innermost to outermost for exited loops + // Генерация очистки стека от внутреннего цикла к внешнему при выходе через Перейти for (int i = gotoStack.Count - 1; i >= labelStack.Count; i--) { - if (gotoStack[i].type == BlockForEach) + if (gotoStack[i].type == BlockType.ForEach) AddCommand(OperationCode.StopIterator); - else if (gotoStack[i].type == BlockFor) + else if (gotoStack[i].type == BlockType.For) AddCommand(OperationCode.PopTmp, 1); } }