diff --git a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs index 28b7b4af7..69522b883 100644 --- a/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs +++ b/src/ScriptEngine/Compiler/CodeGeneratorPrivateTypes.cs @@ -42,5 +42,35 @@ public static NestedLoopInfo New() public List breakStatements; public int tryNesting; } + + private enum BlockType + { + While, + ForEach, + For, + If, + ElseIf, + Else, + Try, + Except + } + + private class LabelInfo + { + public int codeIndex = DUMMY_ADDRESS; + public List<(BlockType type, int id)> blockStack; + public int tryNesting; + } + + private struct PendingGoto + { + public int commandIndex; + public int exitTryIndex; + public string labelName; + public List<(BlockType type, int id)> blockStack; + public List<(int commandIndex, BlockType 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..05c77600e 100644 --- a/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs +++ b/src/ScriptEngine/Compiler/StackMachineCodeGenerator.cs @@ -41,6 +41,11 @@ 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<(BlockType type, int id)> _blockStack = new List<(BlockType type, int id)>(); + private int _tryNestingCount; + private int _blockIdCounter; private IBslProcess _compilerProcess; @@ -197,6 +202,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 +217,7 @@ protected override void VisitModuleBody(BslSyntaxNode child) throw; } + FinalizePendingGotos(); _ctx.PopScope(); var topIdx = _ctx.ScopeCount - 1; @@ -249,12 +256,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) + { + // Обратный переход, метка уже определена + 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 + { + // Прямой переход, метка еще не определена + if (!_labels.ContainsKey(labelName)) + _labels[labelName] = new LabelInfo(); + + var currentStack = SnapshotBlockStack(); + + // Резервируем слоты очистки для циклов (от внутреннего к внешнему) + 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 == BlockType.ForEach || block.type == BlockType.For) + { + 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; + + // Разрешаем отложенные прямые переходы, указывающие на эту метку + 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 + { + // Заполняем слоты очистки циклов + 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 == BlockType.ForEach) + CorrectCommand(slot.commandIndex, OperationCode.StopIterator, 0); + else if (slot.loopType == BlockType.For) + 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 +445,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 +498,17 @@ protected override void VisitWhileNode(WhileLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = conditionIndex; _nestedLoops.Push(loopRecord); + PushBlock(BlockType.While); 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 +526,16 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = loopBegin; _nestedLoops.Push(loopRecord); - + PushBlock(BlockType.ForEach); + 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 +566,7 @@ protected override void VisitForLoopNode(ForLoopNode node) var loopRecord = NestedLoopInfo.New(); loopRecord.startPoint = indexLoopBegin; _nestedLoops.Push(loopRecord); + PushBlock(BlockType.For); VisitCodeBlock(node.LoopBody); VisitBlockEnd(node.EndLocation); @@ -456,6 +576,7 @@ protected override void VisitForLoopNode(ForLoopNode node) var indexLoopEnd = AddCommand(OperationCode.PopTmp, 1); CorrectCommandArgument(conditionIndex, indexLoopEnd); + PopBlock(); CorrectBreakStatements(_nestedLoops.Pop(), indexLoopEnd); } @@ -504,11 +625,13 @@ protected override void VisitIfNode(ConditionNode node) var jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); + PushBlock(BlockType.If); 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 +640,9 @@ protected override void VisitIfNode(ConditionNode node) AddLineNumber(alternative.Location.LineNumber); VisitIfExpression(elif.Expression); jumpFalseIndex = AddCommand(OperationCode.JmpFalse, DUMMY_ADDRESS); + PushBlock(BlockType.ElseIf); VisitIfTruePart(elif.TruePart); + PopBlock(); exitIndices.Add(AddCommand(OperationCode.Jmp, DUMMY_ADDRESS)); } else @@ -525,7 +650,9 @@ protected override void VisitIfNode(ConditionNode node) hasAlternativeBranches = true; CorrectCommandArgument(jumpFalseIndex, _module.Code.Count); AddLineNumber(alternative.Location.LineNumber, CodeGenerationFlags.CodeStatistics); + PushBlock(BlockType.Else); VisitCodeBlock(alternative); + PopBlock(); } } @@ -850,17 +977,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 +1092,19 @@ protected override void VisitTryExceptNode(TryExceptNode node) protected override void VisitTryBlock(CodeBatchNode node) { PushTryNesting(); + PushBlock(BlockType.Try); base.VisitTryBlock(node); + PopBlock(); PopTryNesting(); } + protected override void VisitExceptBlock(CodeBatchNode node) + { + PushBlock(BlockType.Except); + base.VisitExceptBlock(node); + PopBlock(); + } + protected override void VisitExecuteStatement(BslSyntaxNode node) { base.VisitExecuteStatement(node); @@ -1066,8 +1202,8 @@ private void MakeNewObjectStatic(NewObjectNode node) { PushCallArguments(node.ConstructorArguments); } - else - { + else + { AddCommand(OperationCode.ArgNum, 0); } @@ -1097,7 +1233,74 @@ private void PopTryNesting() _nestedLoops.Peek().tryNesting--; } } - + + private void PushBlock(BlockType blockType) + { + _blockStack.Add((blockType, _blockIdCounter++)); + if (blockType == BlockType.Try) + _tryNestingCount++; + } + + private void PopBlock() + { + var last = _blockStack[_blockStack.Count - 1]; + _blockStack.RemoveAt(_blockStack.Count - 1); + if (last.type == BlockType.Try) + _tryNestingCount--; + } + + private List<(BlockType type, int id)> SnapshotBlockStack() + { + return new List<(BlockType type, int id)>(_blockStack); + } + + private static bool IsValidGotoTarget(List<(BlockType type, int id)> gotoStack, List<(BlockType 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<(BlockType type, int id)> gotoStack, List<(BlockType type, int id)> labelStack) + { + // Генерация очистки стека от внутреннего цикла к внешнему при выходе через Перейти + for (int i = gotoStack.Count - 1; i >= labelStack.Count; i--) + { + if (gotoStack[i].type == BlockType.ForEach) + AddCommand(OperationCode.StopIterator); + else if (gotoStack[i].type == BlockType.For) + 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 +1522,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: + Возврат "три"; + + ~МеткаДругое: + Возврат "другое"; +КонецФункции