Add named function parameters

This commit is contained in:
Stone_Red
2026-08-06 21:00:18 +02:00
parent e7bf9591ba
commit 6bc5d01a2e
10 changed files with 220 additions and 15 deletions
+2 -3
View File
@@ -45,9 +45,8 @@ func greet:
print_line Hello, ${name}! print_line Hello, ${name}!
end_func end_func
func add: func add: a, b
var result = %in + %in calc return ${a} + ${b} calc
return ${result}
call greet with Alice call greet with Alice
call greet with Bob call greet with Bob
+3 -1
View File
@@ -78,7 +78,7 @@ print_line ${value}
| Code flow | `lbl name` | `label name:` | Label declaration/target. | | Code flow | `lbl name` | `label name:` | Label declaration/target. |
| Code flow | `jmp name` | `goto name` | Unconditional jump. | | Code flow | `jmp name` | `goto name` | Unconditional jump. |
| Code flow | `jif name \| cond` | `if cond goto name` | Conditional 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` | `call name` | Function call without args. |
| Functions | `cal name \| a,b,c` | `call name with a, b, c` | Function call with 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. | | 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`. - Postfix operations are `calc`, `eval`, and `task`.
- `return` exits the function early (optionally with a value via `return <value>`); - `return` exits the function early (optionally with a value via `return <value>`);
`end_func` is the explicit end-of-function marker. Both terminate the current function call. `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`.
+24
View File
@@ -370,6 +370,30 @@ func add:
end_func end_func
``` ```
#### Named parameters
```
func <name>: <param1>, <param2>, …
<body>
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 <name> 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` ### Calling a function - `call`
``` ```
+1 -1
View File
@@ -279,7 +279,7 @@ internal class TextEditor
if (!isTerminatingStatement && ( if (!isTerminatingStatement && (
(trimmed.StartsWith("if ", StringComparison.Ordinal) && trimmed.EndsWith(':')) (trimmed.StartsWith("if ", StringComparison.Ordinal) && trimmed.EndsWith(':'))
|| (trimmed.StartsWith("while ", 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:")) || trimmed == "else:"))
{ {
if (trimmed.StartsWith("if ", StringComparison.Ordinal)) if (trimmed.StartsWith("if ", StringComparison.Ordinal))
@@ -320,4 +320,104 @@ public class FunctionStatementsTests
YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function"); YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function");
} }
// --- Named parameter tests ---
[TestMethod]
public void FunctionNamedParametersTest()
{
List<string> 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<string> 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<string> 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<string> lines =
[
"func add: a b"
];
YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax");
}
[TestMethod]
public void FunctionNamedParametersExtraArgumentsRemainTest()
{
List<string> 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<string> lines =
[
"call add with 3, 4",
"func add: a, b",
"return ${a} + ${b} calc",
"var total = %out",
"${total}"
];
YesNtAssert.IsLastLineEqual(lines, "7");
}
} }
@@ -27,6 +27,7 @@ internal sealed class RuntimeInformation : IStatementContext
public Dictionary<string, string> GlobalVariables { get; set; } = []; public Dictionary<string, string> GlobalVariables { get; set; } = [];
public Dictionary<string, int> Functions { get; } = []; public Dictionary<string, int> Functions { get; } = [];
public Dictionary<string, List<string>> FunctionParameters { get; } = [];
public Dictionary<int, int> BlockBoundaries { get; } = []; public Dictionary<int, int> BlockBoundaries { get; } = [];
internal Action PreScanLinesAction { get; set; } internal Action PreScanLinesAction { get; set; }
public Stack<FunctionScope> FunctionCallStack { get; } = new(); public Stack<FunctionScope> FunctionCallStack { get; } = new();
@@ -176,6 +177,7 @@ internal sealed class RuntimeInformation : IStatementContext
GlobalVariables.Clear(); GlobalVariables.Clear();
Labels.Clear(); Labels.Clear();
Functions.Clear(); Functions.Clear();
FunctionParameters.Clear();
BlockBoundaries.Clear(); BlockBoundaries.Clear();
FunctionCallStack.Clear(); FunctionCallStack.Clear();
InParametersStack.Clear(); InParametersStack.Clear();
@@ -1,5 +1,8 @@
namespace YesNt.Interpreter.Runtime; namespace YesNt.Interpreter.Runtime;
using System;
using System.Collections.Generic;
/// <summary> /// <summary>
/// Base class for all classes that host statement handler methods. /// Base class for all classes that host statement handler methods.
/// Subclasses declare methods decorated with <see cref="Attributes.StatementAttribute"/> or /// Subclasses declare methods decorated with <see cref="Attributes.StatementAttribute"/> or
@@ -21,4 +24,78 @@ internal abstract class StatementRuntimeInformation
{ {
return value.Trim().TrimEnd(':').Trim(); return value.Trim().TrimEnd(':').Trim();
} }
/// <summary>
/// Parses a function declaration (the text after the <c>func</c> keyword), extracting the
/// function name and its optional comma-separated named parameters.
/// </summary>
/// <returns>
/// <see langword="true"/> if the declaration is well formed; otherwise <see langword="false"/>.
/// </returns>
internal static bool TryParseFunctionSignature(string declaration, out string name, out List<string> 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;
}
/// <summary>
/// Binds a function's named parameters to local variables by popping values from the
/// current call scope's argument stack, mirroring <c>var p = %in</c> for each parameter.
/// </summary>
protected void BindNamedParameters(string functionKey)
{
if (!RuntimeInfo.FunctionParameters.TryGetValue(functionKey, out List<string> 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();
}
}
} }
@@ -597,6 +597,7 @@ public class YesNtInterpreter
internal void PreScanLines() internal void PreScanLines()
{ {
runtimeInfo.BlockBoundaries.Clear(); runtimeInfo.BlockBoundaries.Clear();
runtimeInfo.FunctionParameters.Clear();
lineMatchingHandlers = new List<List<StatementHandler>>(runtimeInfo.Lines.Count); lineMatchingHandlers = new List<List<StatementHandler>>(runtimeInfo.Lines.Count);
// Dictionary to track open blocks by their expected end statement name // Dictionary to track open blocks by their expected end statement name
@@ -615,6 +616,12 @@ public class YesNtInterpreter
{ {
matchingHandlers.Add(handler); matchingHandlers.Add(handler);
if (handler.Attribute.Name == "func"
&& StatementRuntimeInformation.TryParseFunctionSignature(content["func ".Length..], out string functionName, out List<string> functionParameters))
{
runtimeInfo.FunctionParameters[functionName] = functionParameters;
}
// Track block starts (skip intermediates — they are handled separately below) // Track block starts (skip intermediates — they are handled separately below)
string blockPair = handler.Attribute.BlockPair; string blockPair = handler.Attribute.BlockPair;
if (!string.IsNullOrEmpty(blockPair) && !handler.Attribute.IsBlockIntermediate) if (!string.IsNullOrEmpty(blockPair) && !handler.Attribute.IsBlockIntermediate)
@@ -248,6 +248,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation
{ {
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack))); RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
RuntimeInfo.InParametersStack.Clear(); RuntimeInfo.InParametersStack.Clear();
BindNamedParameters(key);
if (RuntimeInfo.Functions.TryGetValue(key, out int value)) if (RuntimeInfo.Functions.TryGetValue(key, out int value))
{ {
@@ -19,17 +19,9 @@ internal class FunctionStatements : StatementRuntimeInformation
return; return;
} }
string functionDeclaration = args.Trim(); if (!TryParseFunctionSignature(args, out string key, out _))
if (!functionDeclaration.EndsWith(':'))
{ {
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); RuntimeInfo.Exit(args.Trim().Contains(':') ? ExitMessages.InvalidSyntax : ExitMessages.InvalidSyntaxColonRequired, true);
return;
}
string key = NormalizeBlockName(functionDeclaration);
if (string.IsNullOrWhiteSpace(key))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return; return;
} }
@@ -83,6 +75,7 @@ internal class FunctionStatements : StatementRuntimeInformation
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack))); RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
RuntimeInfo.InParametersStack.Clear(); RuntimeInfo.InParametersStack.Clear();
BindNamedParameters(key);
RuntimeInfo.CurrentLine = string.Empty; RuntimeInfo.CurrentLine = string.Empty;
if (RuntimeInfo.Functions.TryGetValue(key, out int value)) if (RuntimeInfo.Functions.TryGetValue(key, out int value))