aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Runs/Scripts/Vars.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Runs/Scripts/Vars.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core/Runs/Scripts/Vars.cs')
-rw-r--r--src/Godosa.Core/Runs/Scripts/Vars.cs174
1 files changed, 174 insertions, 0 deletions
diff --git a/src/Godosa.Core/Runs/Scripts/Vars.cs b/src/Godosa.Core/Runs/Scripts/Vars.cs
new file mode 100644
index 0000000..a01da62
--- /dev/null
+++ b/src/Godosa.Core/Runs/Scripts/Vars.cs
@@ -0,0 +1,174 @@
+// Provenance: own design — shared run scripts (spec §3.3)
+using System.Text;
+
+namespace Godosa.Core.Runs.Scripts;
+
+public static class VarsFile
+{
+ public static (Dictionary<string, string> Values, List<ScriptError> Errors) Parse(string path, string text)
+ {
+ var values = new Dictionary<string, string>(StringComparer.Ordinal);
+ var errors = new List<ScriptError>();
+ var lines = text.TrimStart('\uFEFF').Replace("\r\n", "\n").Split('\n');
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var line = StepLexer.StripComment(lines[i]).Trim();
+ if (line.Length == 0)
+ continue;
+ var eq = line.IndexOf('=');
+ var name = eq > 0 ? line[..eq].Trim() : "";
+ if (name.Length == 0 || name.Contains(' '))
+ {
+ errors.Add(new ScriptError(new SourceRef(path, i + 1), "bad vars line (name = value)"));
+ continue;
+ }
+ values[name] = line[(eq + 1)..].Trim();
+ }
+ return (values, errors);
+ }
+}
+
+/// <summary><c>${name}</c> expansion, innermost first (<c>${ui.rest.${hours}}</c>); <c>${probe:name(args)}</c> = a probe
+/// value; a <c>$</c> not followed by <c>{</c> is literal.</summary>
+public static class VarExpander
+{
+ public static string Expand(string text, Func<string, string?> lookup, Func<string, RunValue>? probe)
+ {
+ var sb = new StringBuilder();
+ var i = 0;
+ while (i < text.Length)
+ {
+ if (text[i] == '$' && i + 1 < text.Length && text[i + 1] == '{')
+ {
+ i += 2;
+ sb.Append(Reference(text, ref i, lookup, probe));
+ continue;
+ }
+ sb.Append(text[i++]);
+ }
+ return sb.ToString();
+ }
+
+ /// <summary>Resolves the reference opened before <paramref name="i"/> (nested references in its name first); the
+ /// value is used as is, never scanned again.</summary>
+ private static string Reference(string text, ref int i, Func<string, string?> lookup, Func<string, RunValue>? probe)
+ {
+ var name = new StringBuilder();
+ while (true)
+ {
+ if (i >= text.Length)
+ throw new ScriptFault($"unclosed ${{ in '{text}'");
+ if (text[i] == '}')
+ {
+ i++;
+ break;
+ }
+ if (text[i] == '$' && i + 1 < text.Length && text[i + 1] == '{')
+ {
+ i += 2;
+ name.Append(Reference(text, ref i, lookup, probe));
+ continue;
+ }
+ name.Append(text[i++]);
+ }
+ var n = name.ToString();
+ if (n.Length == 0)
+ throw new ScriptFault("empty ${}");
+ if (n.StartsWith("probe:", StringComparison.Ordinal))
+ return (probe ?? throw new ScriptFault($"no probes here: ${{{n}}}"))(n["probe:".Length..]).ToString();
+ return lookup(n) ?? throw new ScriptFault($"unresolved variable ${{{n}}}");
+ }
+
+ /// <summary>Plain names inside the innermost <c>${…}</c> (no nested <c>${</c>, no <c>probe:</c>).</summary>
+ public static IEnumerable<string> Names(string text)
+ {
+ for (var i = 0; i < text.Length - 1; i++)
+ {
+ if (text[i] != '$' || text[i + 1] != '{')
+ continue;
+ var close = text.IndexOf('}', i + 2);
+ if (close < 0)
+ yield break;
+ var inner = text[(i + 2)..close];
+ if (inner.Length > 0 && !inner.Contains("${", StringComparison.Ordinal) && !inner.StartsWith("probe:", StringComparison.Ordinal))
+ yield return inner;
+ }
+ }
+}
+
+public sealed record ScopeState(Dictionary<string, string> Vars, bool Local, OutputLevel Level);
+
+/// <summary>Variable scopes: root (globals), include scopes (vars go to root, output level scoped), def-call scopes
+/// (local vars). Lookup walks from the top down.</summary>
+public sealed class VarScopes
+{
+ private readonly List<ScopeState> _stack;
+
+ public VarScopes(OutputLevel level) => _stack = [new ScopeState(new Dictionary<string, string>(StringComparer.Ordinal), false, level)];
+
+ public int Depth => _stack.Count;
+
+ public OutputLevel Level
+ {
+ get => _stack[^1].Level;
+ set => _stack[^1] = _stack[^1] with { Level = value };
+ }
+
+ public void Push(bool local, OutputLevel level, IEnumerable<KeyValuePair<string, string>> bindings) =>
+ _stack.Add(new ScopeState(new Dictionary<string, string>(bindings, StringComparer.Ordinal), local, level));
+
+ public void Pop()
+ {
+ if (_stack.Count > 1)
+ _stack.RemoveAt(_stack.Count - 1);
+ }
+
+ public void PopTo(int depth)
+ {
+ while (_stack.Count > Math.Max(1, depth))
+ _stack.RemoveAt(_stack.Count - 1);
+ }
+
+ public string? Get(string name)
+ {
+ for (var i = _stack.Count - 1; i >= 0; i--)
+ {
+ if (_stack[i].Vars.TryGetValue(name, out var v))
+ return v;
+ }
+ return null;
+ }
+
+ public void Set(string name, string value)
+ {
+ for (var i = _stack.Count - 1; i >= 1; i--)
+ {
+ if (_stack[i].Local)
+ {
+ _stack[i].Vars[name] = value;
+ return;
+ }
+ }
+ _stack[0].Vars[name] = value;
+ }
+
+ public Dictionary<string, string> Visible()
+ {
+ var all = new Dictionary<string, string>(StringComparer.Ordinal);
+ foreach (var scope in _stack)
+ foreach (var (k, v) in scope.Vars)
+ all[k] = v;
+ return all;
+ }
+
+ public List<ScopeState> Snapshot() =>
+ [.. _stack.Select(s => s with { Vars = new Dictionary<string, string>(s.Vars, StringComparer.Ordinal) })];
+
+ public void Restore(IReadOnlyList<ScopeState> scopes)
+ {
+ _stack.Clear();
+ _stack.AddRange(scopes.Select(s => s with { Vars = new Dictionary<string, string>(s.Vars, StringComparer.Ordinal) }));
+ if (_stack.Count == 0)
+ _stack.Add(new ScopeState(new Dictionary<string, string>(StringComparer.Ordinal), false, OutputLevel.Labels));
+ }
+}