Files
fabric-carpet/src/main/java/carpet/script/ScriptHost.java
T
altrisi 5e8b4ece5b Migrate nullability annotations to jSpecify
Mojang has done that, we don't need 3 different sets of annotations
2025-12-01 13:46:33 +01:00

582 lines
19 KiB
Java

package carpet.script;
import carpet.script.exception.ExpressionException;
import carpet.script.exception.IntegrityException;
import carpet.script.exception.InternalExpressionException;
import carpet.script.value.FunctionValue;
import carpet.script.value.Value;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jspecify.annotations.Nullable;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;
public abstract class ScriptHost
{
private static final Map<Long, Random> randomizers = new Long2ObjectOpenHashMap<>();
public static Thread mainThread = null;
private final Map<Value, ThreadPoolExecutor> executorServices = new HashMap<>();
private final Map<Value, Object> locks = new ConcurrentHashMap<>();
private final ScriptServer scriptServer;
protected boolean inTermination = false;
public boolean strict;
public Expression.LoadOverride loadOverrides;
public Expression.@Nullable ExpressionNode root;
private final Set<String> deprecations = new HashSet<>();
public Random getRandom(long aLong)
{
if (randomizers.size() > 65536)
{
randomizers.clear();
}
return randomizers.computeIfAbsent(aLong, Random::new);
}
public boolean resetRandom(long aLong)
{
return randomizers.remove(aLong) != null;
}
public Path resolveScriptFile(String suffix)
{
return scriptServer.resolveResource(suffix);
}
public boolean canSynchronouslyExecute()
{
return true;
}
public static class ModuleData
{
Module parent;
public final Map<String, FunctionValue> globalFunctions = new Object2ObjectOpenHashMap<>();
public final Map<String, LazyValue> globalVariables = new Object2ObjectOpenHashMap<>();
public final Map<String, ModuleData> functionImports = new Object2ObjectOpenHashMap<>(); // imported functions string to module
public final Map<String, ModuleData> globalsImports = new Object2ObjectOpenHashMap<>(); // imported global variables string to module
public final Map<String, ModuleData> futureImports = new Object2ObjectOpenHashMap<>(); // imports not known before used
public ModuleData(Module parent, ModuleData other)
{
super();
// imports are just pointers, but they still point to the wrong modules (point to the parent)
this.parent = parent;
globalFunctions.putAll(other.globalFunctions);
other.globalVariables.forEach((key, value) ->
{
Value var = value.evalValue(null);
Value copy = var.deepcopy();
copy.boundVariable = var.boundVariable;
globalVariables.put(key, (c, t) -> copy);
});
}
public void setImportsBasedOn(ScriptHost host, ModuleData other)
{
// fixing imports
other.functionImports.forEach((name, targetData) -> {
functionImports.put(name, host.moduleData.get(targetData.parent));
});
other.globalsImports.forEach((name, targetData) -> {
globalsImports.put(name, host.moduleData.get(targetData.parent));
});
other.futureImports.forEach((name, targetData) -> {
futureImports.put(name, host.moduleData.get(targetData.parent));
});
}
public ModuleData(Module parent)
{
this.parent = parent;
}
}
protected final Map<String, ScriptHost> userHosts = new Object2ObjectOpenHashMap<>();
private final Map<Module, ModuleData> moduleData = new HashMap<>(); // marking imports
private final Map<String, Module> modules = new HashMap<>();
protected ScriptHost parent;
protected boolean perUser;
public String user;
public String getName()
{
return main == null ? null : main.name();
}
public String getVisualName()
{
return main == null ? "built-in default app" : main.name();
}
public boolean isDefaultApp()
{
return main == null;
}
@Nullable
public final Module main;
@FunctionalInterface
public interface ErrorSnooper
{
List<String> apply(Expression expression, Token token, Context context, String message);
}
public ErrorSnooper errorSnooper = null;
protected ScriptHost(@Nullable Module code, ScriptServer scriptServer, boolean perUser, ScriptHost parent, Expression.LoadOverride loadOverrides)
{
this.parent = parent;
this.main = code;
this.perUser = perUser;
this.user = null;
this.strict = false;
this.loadOverrides = loadOverrides;
this.scriptServer = scriptServer;
ModuleData moduleData = new ModuleData(code);
initializeModuleGlobals(moduleData);
this.moduleData.put(code, moduleData);
this.modules.put(code == null ? null : code.name(), code);
mainThread = Thread.currentThread();
}
void initializeModuleGlobals(ModuleData md)
{
}
public void importModule(Context c, String moduleName)
{
if (modules.containsKey(moduleName.toLowerCase(Locale.ROOT)))
{
return; // aready imported
}
Module module = getModuleOrLibraryByName(moduleName);
if (modules.containsKey(module.name()))
{
return; // aready imported, once again, in case some discrepancies in names?
}
modules.put(module.name(), module);
ModuleData data = new ModuleData(module);
initializeModuleGlobals(data);
moduleData.put(module, data);
runModuleCode(c, module);
//moduleData.remove(module); // we are pooped already, but doesn't hurt to clean that up.
//modules.remove(module.getName());
//throw new InternalExpressionException("Failed to import a module "+moduleName);
}
public void importNames(Context c, Module targetModule, String sourceModuleName, List<String> identifiers)
{
if (!moduleData.containsKey(targetModule))
{
throw new InternalExpressionException("Cannot import to module that doesn't exist");
}
Module source = modules.get(sourceModuleName);
ModuleData sourceData = moduleData.get(source);
ModuleData targetData = moduleData.get(targetModule);
if (sourceData == null || targetData == null)
{
throw new InternalExpressionException("Cannot import from module that is not imported");
}
for (String identifier : identifiers)
{
if (sourceData.globalFunctions.containsKey(identifier))
{
targetData.functionImports.put(identifier, sourceData);
}
else if (sourceData.globalVariables.containsKey(identifier))
{
targetData.globalsImports.put(identifier, sourceData);
}
else
{
targetData.futureImports.put(identifier, sourceData);
}
}
}
public Stream<String> availableImports(String moduleName)
{
Module source = modules.get(moduleName);
ModuleData sourceData = moduleData.get(source);
if (sourceData == null)
{
throw new InternalExpressionException("Cannot import from module that is not imported");
}
return Stream.concat(
globalVariableNames(source, s -> s.startsWith("global_")),
globalFunctionNames(source, s -> true)
).distinct().sorted();
}
protected abstract Module getModuleOrLibraryByName(String name); // this should be shell out in the executor
protected abstract void runModuleCode(Context c, Module module); // this should be shell out in the executor
@Nullable
public FunctionValue getFunction(String name)
{
return getFunction(main, name);
}
public FunctionValue getAssertFunction(Module module, String name)
{
FunctionValue ret = getFunction(module, name);
if (ret == null)
{
if (module == main)
{
throw new InternalExpressionException("Function '" + name + "' is not defined yet");
}
else
{
throw new InternalExpressionException("Function '" + name + "' is not defined nor visible by its name in the imported module '" + module.name() + "'");
}
}
return ret;
}
@Nullable
private FunctionValue getFunction(Module module, String name)
{
ModuleData local = getModuleData(module);
FunctionValue ret = local.globalFunctions.get(name); // most uses would be from local scope anyways
if (ret != null)
{
return ret;
}
ModuleData target = local.functionImports.get(name);
if (target != null)
{
ret = target.globalFunctions.get(name);
if (ret != null)
{
return ret;
}
}
// not in local scope - will need to travel over import links
target = local.futureImports.get(name);
if (target == null)
{
return null;
}
target = findModuleDataFromFunctionImports(name, target, 0);
if (target == null)
{
return null;
}
local.futureImports.remove(name);
local.functionImports.put(name, target);
return target.globalFunctions.get(name);
}
@Nullable
private ModuleData findModuleDataFromFunctionImports(String name, ModuleData source, int ttl)
{
if (ttl > 64)
{
throw new InternalExpressionException("Cannot import " + name + ", either your imports are too deep or too loopy");
}
if (source.globalFunctions.containsKey(name))
{
return source;
}
if (source.functionImports.containsKey(name))
{
return findModuleDataFromFunctionImports(name, source.functionImports.get(name), ttl + 1);
}
if (source.futureImports.containsKey(name))
{
return findModuleDataFromFunctionImports(name, source.futureImports.get(name), ttl + 1);
}
return null;
}
public LazyValue getGlobalVariable(String name)
{
return getGlobalVariable(main, name);
}
public LazyValue getGlobalVariable(Module module, String name)
{
ModuleData local = getModuleData(module);
LazyValue ret = local.globalVariables.get(name); // most uses would be from local scope anyways
if (ret != null)
{
return ret;
}
ModuleData target = local.globalsImports.get(name);
if (target != null)
{
ret = target.globalVariables.get(name);
if (ret != null)
{
return ret;
}
}
// not in local scope - will need to travel over import links
target = local.futureImports.get(name);
if (target == null)
{
return null;
}
target = findModuleDataFromGlobalImports(name, target, 0);
if (target == null)
{
return null;
}
local.futureImports.remove(name);
local.globalsImports.put(name, target);
return target.globalVariables.get(name);
}
private ModuleData findModuleDataFromGlobalImports(String name, ModuleData source, int ttl)
{
if (ttl > 64)
{
throw new InternalExpressionException("Cannot import " + name + ", either your imports are too deep or too loopy");
}
if (source.globalVariables.containsKey(name))
{
return source;
}
if (source.globalsImports.containsKey(name))
{
return findModuleDataFromGlobalImports(name, source.globalsImports.get(name), ttl + 1);
}
if (source.futureImports.containsKey(name))
{
return findModuleDataFromGlobalImports(name, source.futureImports.get(name), ttl + 1);
}
return null;
}
public void delFunctionWithPrefix(Module module, String prefix)
{
ModuleData data = getModuleData(module);
data.globalFunctions.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
data.functionImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
}
public void delFunction(Module module, String funName)
{
ModuleData data = getModuleData(module);
data.globalFunctions.remove(funName);
data.functionImports.remove(funName);
}
public void delGlobalVariableWithPrefix(Module module, String prefix)
{
ModuleData data = getModuleData(module);
data.globalVariables.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
data.globalsImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
}
public void delGlobalVariable(Module module, String varName)
{
ModuleData data = getModuleData(module);
data.globalFunctions.remove(varName);
data.functionImports.remove(varName);
}
private ModuleData getModuleData(Module module)
{
ModuleData data = moduleData.get(module);
if (data == null)
{
throw new IntegrityException("Module structure changed for the app. Did you reload the app with tasks running?");
}
return data;
}
protected void assertAppIntegrity(Module module)
{
getModuleData(module);
}
public void addUserDefinedFunction(Context ctx, Module module, String name, FunctionValue fun)
{
getModuleData(module).globalFunctions.put(name, fun);
}
public void setGlobalVariable(Module module, String name, LazyValue lv)
{
getModuleData(module).globalVariables.put(name, lv);
}
public Stream<String> globalVariableNames(Module module, Predicate<String> predicate)
{
return Stream.concat(Stream.concat(
getModuleData(module).globalVariables.keySet().stream(),
getModuleData(module).globalsImports.keySet().stream()
), getModuleData(module).futureImports.keySet().stream().filter(s -> s.startsWith("global_"))).filter(predicate);
}
public Stream<String> globalFunctionNames(Module module, Predicate<String> predicate)
{
return Stream.concat(Stream.concat(
getModuleData(module).globalFunctions.keySet().stream(),
getModuleData(module).functionImports.keySet().stream()
), getModuleData(module).futureImports.keySet().stream().filter(s -> !s.startsWith("global_"))).filter(predicate);
}
public ScriptHost retrieveForExecution(String /*Nullable*/ user)
{
if (!perUser)
{
return this;
}
ScriptHost oldUserHost = userHosts.get(user);
if (oldUserHost != null)
{
return oldUserHost;
}
ScriptHost userHost = this.duplicate();
userHost.user = user;
this.setupUserHost(userHost);
userHosts.put(user, userHost);
return userHost;
}
protected void setupUserHost(ScriptHost host)
{
// adding imports
host.modules.putAll(this.modules);
this.moduleData.forEach((key, value) -> host.moduleData.put(key, new ModuleData(key, value)));
// fixing imports
host.moduleData.forEach((module, data) -> data.setImportsBasedOn(host, this.moduleData.get(data.parent)));
host.root = this.root;
}
public synchronized void handleExpressionException(String msg, ExpressionException exc)
{
System.out.println(msg + ": " + exc);
}
protected abstract ScriptHost duplicate();
public Object getLock(Value name)
{
return locks.computeIfAbsent(name, n -> new Object());
}
public ThreadPoolExecutor getExecutor(Value pool)
{
if (inTermination)
{
return null;
}
return executorServices.computeIfAbsent(pool, v -> (ThreadPoolExecutor) Executors.newCachedThreadPool());
}
public int taskCount()
{
return executorServices.values().stream().map(ThreadPoolExecutor::getActiveCount).reduce(0, Integer::sum);
}
public int taskCount(Value pool)
{
return executorServices.containsKey(pool) ? executorServices.get(pool).getActiveCount() : 0;
}
public void onClose()
{
inTermination = true;
executorServices.values().forEach(ThreadPoolExecutor::shutdown);
for (ScriptHost uh : userHosts.values())
{
uh.onClose();
}
if (taskCount() > 0)
{
executorServices.values().forEach(e -> {
ExecutorService stopper = Executors.newSingleThreadExecutor();
stopper.submit(() -> {
try
{
// Wait a while for existing tasks to terminate
if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS))
{
e.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS))
{
CarpetScriptServer.LOG.error("Failed to stop app's thread");
}
}
}
catch (InterruptedException ie)
{
// (Re-)Cancel if current thread also interrupted
e.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
stopper.shutdown();
stopper.shutdownNow();
});
});
}
}
public void setPerPlayer(boolean isPerUser)
{
perUser = isPerUser;
}
public boolean isPerUser()
{
return perUser;
}
public Set<String> getUserList()
{
return userHosts.keySet();
}
public void resetErrorSnooper()
{
errorSnooper = null;
}
public static final Logger DEPRECATION_LOG = LoggerFactory.getLogger("Scarpet Deprecation Warnings");
public boolean issueDeprecation(String feature)
{
if (deprecations.contains(feature))
{
return false;
}
deprecations.add(feature);
DEPRECATION_LOG.warn("App '" + getVisualName() + "' uses '" + feature + "', which is deprecated for removal. Check the docs for a replacement");
return true;
}
public ScriptServer scriptServer()
{
return scriptServer;
}
}