diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Runs/Scripts/Vars.cs | |
| download | godosa-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.cs | 174 |
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)); + } +} |
