// Provenance: own design — shared run scripts namespace Godosa.Core.Runs.Scripts; public enum Op { Verb, Wait, Expect, Set, Checkpoint, LabelBegin, LabelEnd, ScopeBegin, ScopeEnd, TryBegin, TryEnd, SetupBegin, SetupEnd } /// One flattened instruction. = include / call sites, outermost first. public sealed record Instr(Op Op, SourceRef At, string Text, IReadOnlyList Chain, int Format) { public string A { get; init; } = ""; public string B { get; init; } = ""; public int Timeout { get; init; } public IReadOnlyList> Bindings { get; init; } = []; public bool LocalVars { get; init; } public int Jump { get; init; } = -1; } public sealed record ScriptProgram(string Entry, RunMode Mode, ulong? Seed, IReadOnlyDictionary Vars, IReadOnlySet Locked, IReadOnlyList Code, IReadOnlyList Errors); public interface IScriptSource { string? Read(string path); } public sealed class MemoryScriptSource(IReadOnlyDictionary files) : IScriptSource { public string? Read(string path) => files.GetValueOrDefault(path); } /// Files under ; paths are '/'-separated and relative to it. public sealed class FileScriptSource(string root) : IScriptSource { public string? Read(string path) { var full = Path.GetFullPath(Path.Combine(root, path)); return File.Exists(full) ? File.ReadAllText(full) : null; } } public static class ScriptPaths { public static string Resolve(string fromFile, string relative) { var dir = fromFile.Contains('/') ? fromFile[..fromFile.LastIndexOf('/')] : ""; var parts = new List(dir.Length == 0 ? [] : dir.Split('/')); foreach (var part in relative.Replace('\\', '/').Split('/')) { if (part is "" or ".") continue; if (part == ".." && parts.Count > 0) parts.RemoveAt(parts.Count - 1); else if (part != "..") parts.Add(part); } return string.Join('/', parts); } }