using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting;
///
/// Compiles + runs user scripts against a subclass. Core
/// evaluator — no caching, no timeout, no logging side-effects yet (those land in
/// Stream A.3, A.4, A.5 respectively). Stream B + C wrap this with the dependency
/// scheduler + alarm state machine.
///
///
///
/// Scripts are compiled against so the
/// context member is named ctx in the script, matching the
/// 's walker and the Admin UI type stub.
///
///
/// Compile pipeline is a three-step gate: (1) Roslyn compile — catches syntax
/// errors + type-resolution failures, throws ;
/// (2) runs against the semantic model —
/// catches sandbox escapes that slipped past reference restrictions due to .NET's
/// type forwarding, throws ; (3)
/// delegate creation — throws at this layer only for internal Roslyn bugs, not
/// user error.
///
///
/// Runtime exceptions thrown from user code propagate unwrapped. The virtual-tag
/// engine (Stream B) catches them per-tag + maps to BadInternalError
/// quality per Phase 7 decision #11 — this layer doesn't swallow anything so
/// tests can assert on the original exception type.
///
///
public sealed class ScriptEvaluator
where TContext : ScriptContext
{
private readonly ScriptRunner _runner;
private ScriptEvaluator(ScriptRunner runner)
{
_runner = runner;
}
public static ScriptEvaluator Compile(string scriptSource)
{
if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource));
var options = ScriptSandbox.Build(typeof(TContext));
var script = CSharpScript.Create(
code: scriptSource,
options: options,
globalsType: typeof(ScriptGlobals));
// Step 1 — Roslyn compile. Throws CompilationErrorException on syntax / type errors.
var diagnostics = script.Compile();
// Step 2 — forbidden-type semantic analysis. Defense-in-depth against reference-list
// leaks due to type forwarding.
var rejections = ForbiddenTypeAnalyzer.Analyze(script.GetCompilation());
if (rejections.Count > 0)
throw new ScriptSandboxViolationException(rejections);
// Step 3 — materialize the callable delegate.
var runner = script.CreateDelegate();
return new ScriptEvaluator(runner);
}
/// Run against an already-constructed context.
public Task RunAsync(TContext context, CancellationToken ct = default)
{
if (context is null) throw new ArgumentNullException(nameof(context));
var globals = new ScriptGlobals { ctx = context };
return _runner(globals, ct);
}
}