diff --git a/README.md b/README.md index 6768d63..eb23639 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,8 @@ func greet: print_line Hello, ${name}! end_func -func add: - var result = %in + %in calc - return ${result} +func add: a, b + return ${a} + ${b} calc call greet with Alice call greet with Bob diff --git a/SYNTAX_V2.md b/SYNTAX_V2.md index 3fa7726..99bcbd5 100644 --- a/SYNTAX_V2.md +++ b/SYNTAX_V2.md @@ -78,7 +78,7 @@ print_line ${value} | Code flow | `lbl name` | `label name:` | Label declaration/target. | | Code flow | `jmp name` | `goto name` | Unconditional jump. | | Code flow | `jif name \| cond` | `if cond goto name` | Conditional jump. | -| Functions | `fnc name` | `func name:` | Function declaration. | +| Functions | `fnc name` | `func name:` | Function declaration (optional `, ` separated named parameters). | | Functions | `cal name` | `call name` | Function call without args. | | Functions | `cal name \| a,b,c` | `call name with a, b, c` | Function call with args. | | Functions | `in value` | `push_in value` | Push in-arg onto input stack. | @@ -116,4 +116,6 @@ print_line ${value} - Postfix operations are `calc`, `eval`, and `task`. - `return` exits the function early (optionally with a value via `return `); `end_func` is the explicit end-of-function marker. Both terminate the current function call. +- Functions may declare named parameters after the name (e.g. `func add: a, b`); each parameter + is bound to a local variable from the call arguments, equivalent to `var a = %in` / `var b = %in`. diff --git a/docs/language-reference.md b/docs/language-reference.md index 29510f5..23e3d1d 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -370,6 +370,30 @@ func add: end_func ``` +#### Named parameters + +``` +func : , , … + +end_func +``` + +Parameters can be declared after the function name. They are bound to local variables in +declaration order, consuming the values passed to `call with …` (or pushed with `push_in`). +`func add: a, b` is equivalent to starting the body with `var a = %in` followed by `var b = %in`. + +```ynt +func add: a, b + return ${a} + ${b} calc +end_func + +call add with 3, 7 +var total = %out +print_line ${total} +``` + +If fewer arguments are passed than declared parameters, the script terminates with an error. + ### Calling a function - `call` ``` diff --git a/src/YesNt.CodeEditor/Editor.cs b/src/YesNt.CodeEditor/Editor.cs index ea1a70d..abdd8af 100644 --- a/src/YesNt.CodeEditor/Editor.cs +++ b/src/YesNt.CodeEditor/Editor.cs @@ -279,7 +279,7 @@ internal class TextEditor if (!isTerminatingStatement && ( (trimmed.StartsWith("if ", StringComparison.Ordinal) && trimmed.EndsWith(':')) || (trimmed.StartsWith("while ", StringComparison.Ordinal) && trimmed.EndsWith(':')) - || (trimmed.StartsWith("func ", StringComparison.Ordinal) && trimmed.EndsWith(':')) + || (trimmed.StartsWith("func ", StringComparison.Ordinal) && trimmed.Contains(':')) || trimmed == "else:")) { if (trimmed.StartsWith("if ", StringComparison.Ordinal)) diff --git a/src/YesNt.Interpreter.Tests/FunctionStatementsTests.cs b/src/YesNt.Interpreter.Tests/FunctionStatementsTests.cs index ac25c40..e453e66 100644 --- a/src/YesNt.Interpreter.Tests/FunctionStatementsTests.cs +++ b/src/YesNt.Interpreter.Tests/FunctionStatementsTests.cs @@ -320,4 +320,104 @@ public class FunctionStatementsTests YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function"); } + + // --- Named parameter tests --- + + [TestMethod] + public void FunctionNamedParametersTest() + { + List lines = + [ + "goto main", + "func add: a, b", + "return ${a} + ${b} calc", + "label main:", + "call add with 3, 4", + "var total = %out", + "${total}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "7"); + } + + [TestMethod] + public void FunctionNamedParametersRespectOrderTest() + { + List lines = + [ + "goto main", + "func pair: first, second", + "push_out ${first}", + "push_out ${second}", + "end_func", + "label main:", + "call pair with A, B", + "var f = %out", + "var s = %out", + "${f}-${s}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "A-B"); + } + + [TestMethod] + public void FunctionNamedParametersMissingArgumentFailsTest() + { + List lines = + [ + "goto main", + "func add: a, b", + "return ${a} calc", + "label main:", + "call add with 5" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No in argument in stack"); + } + + [TestMethod] + public void FunctionNamedParameterInvalidSyntaxTest() + { + List lines = + [ + "func add: a b" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax"); + } + + [TestMethod] + public void FunctionNamedParametersExtraArgumentsRemainTest() + { + List lines = + [ + "goto main", + "func probe: a", + "global first = ${a}", + "global hasExtra = %has_in", + "push_out ${hasExtra}", + "end_func", + "label main:", + "call probe with A, B", + "var extra = %out", + "${extra}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "True"); + } + + [TestMethod] + public void FunctionNamedParametersSearchEntryTest() + { + List lines = + [ + "call add with 3, 4", + "func add: a, b", + "return ${a} + ${b} calc", + "var total = %out", + "${total}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "7"); + } } \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs b/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs index 0ac8db9..1cfc9ab 100644 --- a/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs +++ b/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs @@ -27,6 +27,7 @@ internal sealed class RuntimeInformation : IStatementContext public Dictionary GlobalVariables { get; set; } = []; public Dictionary Functions { get; } = []; + public Dictionary> FunctionParameters { get; } = []; public Dictionary BlockBoundaries { get; } = []; internal Action PreScanLinesAction { get; set; } public Stack FunctionCallStack { get; } = new(); @@ -176,6 +177,7 @@ internal sealed class RuntimeInformation : IStatementContext GlobalVariables.Clear(); Labels.Clear(); Functions.Clear(); + FunctionParameters.Clear(); BlockBoundaries.Clear(); FunctionCallStack.Clear(); InParametersStack.Clear(); diff --git a/src/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs b/src/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs index 706456c..60a620f 100644 --- a/src/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs +++ b/src/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs @@ -1,5 +1,8 @@ namespace YesNt.Interpreter.Runtime; +using System; +using System.Collections.Generic; + /// /// Base class for all classes that host statement handler methods. /// Subclasses declare methods decorated with or @@ -21,4 +24,78 @@ internal abstract class StatementRuntimeInformation { return value.Trim().TrimEnd(':').Trim(); } + + /// + /// Parses a function declaration (the text after the func keyword), extracting the + /// function name and its optional comma-separated named parameters. + /// + /// + /// if the declaration is well formed; otherwise . + /// + internal static bool TryParseFunctionSignature(string declaration, out string name, out List parameters) + { + name = string.Empty; + parameters = []; + + string trimmed = declaration.Trim(); + int colonIndex = trimmed.IndexOf(':'); + if (colonIndex < 0) + { + return false; + } + + name = NormalizeBlockName(trimmed[..colonIndex]); + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + string parameterSection = trimmed[(colonIndex + 1)..].Trim(); + if (string.IsNullOrWhiteSpace(parameterSection)) + { + return true; + } + + foreach (string rawParameter in parameterSection.Split(',')) + { + string parameter = rawParameter.Trim(); + if (string.IsNullOrWhiteSpace(parameter) || parameter.Contains(' ')) + { + return false; + } + + parameters.Add(parameter); + } + + return true; + } + + /// + /// Binds a function's named parameters to local variables by popping values from the + /// current call scope's argument stack, mirroring var p = %in for each parameter. + /// + protected void BindNamedParameters(string functionKey) + { + if (!RuntimeInfo.FunctionParameters.TryGetValue(functionKey, out List parameters) || parameters.Count == 0) + { + return; + } + + if (RuntimeInfo.FunctionCallStack.Count == 0) + { + return; + } + + FunctionScope scope = RuntimeInfo.FunctionCallStack.Peek(); + foreach (string parameter in parameters) + { + if (scope.Arguments.Count == 0) + { + RuntimeInfo.Exit(ExitMessages.NoInArgumentInStack, true); + return; + } + + scope.Variables[parameter] = scope.Arguments.Pop(); + } + } } \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index 813c34d..1745131 100644 --- a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -597,6 +597,7 @@ public class YesNtInterpreter internal void PreScanLines() { runtimeInfo.BlockBoundaries.Clear(); + runtimeInfo.FunctionParameters.Clear(); lineMatchingHandlers = new List>(runtimeInfo.Lines.Count); // Dictionary to track open blocks by their expected end statement name @@ -615,6 +616,12 @@ public class YesNtInterpreter { matchingHandlers.Add(handler); + if (handler.Attribute.Name == "func" + && StatementRuntimeInformation.TryParseFunctionSignature(content["func ".Length..], out string functionName, out List functionParameters)) + { + runtimeInfo.FunctionParameters[functionName] = functionParameters; + } + // Track block starts (skip intermediates — they are handled separately below) string blockPair = handler.Attribute.BlockPair; if (!string.IsNullOrEmpty(blockPair) && !handler.Attribute.IsBlockIntermediate) diff --git a/src/YesNt.Interpreter/Statements/CodeFlowStatements.cs b/src/YesNt.Interpreter/Statements/CodeFlowStatements.cs index ff6536d..50b734b 100644 --- a/src/YesNt.Interpreter/Statements/CodeFlowStatements.cs +++ b/src/YesNt.Interpreter/Statements/CodeFlowStatements.cs @@ -248,6 +248,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation { RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); RuntimeInfo.InParametersStack.Clear(); + BindNamedParameters(key); if (RuntimeInfo.Functions.TryGetValue(key, out int value)) { diff --git a/src/YesNt.Interpreter/Statements/FunctionStatements.cs b/src/YesNt.Interpreter/Statements/FunctionStatements.cs index fafa39c..1b49ff8 100644 --- a/src/YesNt.Interpreter/Statements/FunctionStatements.cs +++ b/src/YesNt.Interpreter/Statements/FunctionStatements.cs @@ -19,17 +19,9 @@ internal class FunctionStatements : StatementRuntimeInformation return; } - string functionDeclaration = args.Trim(); - if (!functionDeclaration.EndsWith(':')) + if (!TryParseFunctionSignature(args, out string key, out _)) { - RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); - return; - } - - string key = NormalizeBlockName(functionDeclaration); - if (string.IsNullOrWhiteSpace(key)) - { - RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + RuntimeInfo.Exit(args.Trim().Contains(':') ? ExitMessages.InvalidSyntax : ExitMessages.InvalidSyntaxColonRequired, true); return; } @@ -83,6 +75,7 @@ internal class FunctionStatements : StatementRuntimeInformation RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); RuntimeInfo.InParametersStack.Clear(); + BindNamedParameters(key); RuntimeInfo.CurrentLine = string.Empty; if (RuntimeInfo.Functions.TryGetValue(key, out int value))