diff --git a/pom.xml b/pom.xml index 848b3615..79763f1c 100644 --- a/pom.xml +++ b/pom.xml @@ -144,9 +144,9 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.12.1 - 17 + 21 diff --git a/praxiscore-code-services/src/main/java/org/praxislive/code/services/CompilerCommandInstaller.java b/praxiscore-code-services/src/main/java/org/praxislive/code/services/CompilerCommandInstaller.java index 7992c86e..5eef9894 100644 --- a/praxiscore-code-services/src/main/java/org/praxislive/code/services/CompilerCommandInstaller.java +++ b/praxiscore-code-services/src/main/java/org/praxislive/code/services/CompilerCommandInstaller.java @@ -39,7 +39,7 @@ import org.praxislive.script.Env; import org.praxislive.script.InlineCommand; import org.praxislive.script.Namespace; -import org.praxislive.script.impl.AbstractSingleCallFrame; +import org.praxislive.script.AbstractSingleCallFrame; /** * diff --git a/praxiscore-hub-net/src/main/java/org/praxislive/hub/net/internal/HubNetCommands.java b/praxiscore-hub-net/src/main/java/org/praxislive/hub/net/internal/HubNetCommands.java index c1725781..2343ebe3 100644 --- a/praxiscore-hub-net/src/main/java/org/praxislive/hub/net/internal/HubNetCommands.java +++ b/praxiscore-hub-net/src/main/java/org/praxislive/hub/net/internal/HubNetCommands.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -32,10 +32,9 @@ import org.praxislive.script.Command; import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; import org.praxislive.script.StackFrame; -import org.praxislive.script.impl.AbstractSingleCallFrame; +import org.praxislive.script.AbstractSingleCallFrame; /** * @@ -53,7 +52,7 @@ public void install(Map commands) { private final static class ConfigurationCommand implements Command { @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { return new AbstractSingleCallFrame(namespace, args) { @Override protected Call createCall(Env env, List args) throws Exception { diff --git a/praxiscore-hub/src/main/java/org/praxislive/hub/Hub.java b/praxiscore-hub/src/main/java/org/praxislive/hub/Hub.java index 80bcd2f5..4c47d1dc 100644 --- a/praxiscore-hub/src/main/java/org/praxislive/hub/Hub.java +++ b/praxiscore-hub/src/main/java/org/praxislive/hub/Hub.java @@ -43,7 +43,7 @@ import org.praxislive.core.RootHub; import org.praxislive.core.services.Service; import org.praxislive.core.services.Services; -import org.praxislive.script.impl.ScriptServiceImpl; +import org.praxislive.script.DefaultScriptService; /** * Support for configuring and running a {@link RootHub}, along with the @@ -91,7 +91,7 @@ private Hub(Builder builder) { private void extractExtensions(Builder builder, List exts) { exts.add(new DefaultComponentFactoryService()); - exts.add(new ScriptServiceImpl()); + exts.add(new DefaultScriptService()); exts.add(new DefaultTaskService()); exts.addAll(builder.extensions); } diff --git a/praxiscore-script/pom.xml b/praxiscore-script/pom.xml index 106d1bbf..11629de7 100644 --- a/praxiscore-script/pom.xml +++ b/praxiscore-script/pom.xml @@ -12,8 +12,18 @@ - org.junit.vintage - junit-vintage-engine + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine test diff --git a/praxiscore-script/src/main/java/module-info.java b/praxiscore-script/src/main/java/module-info.java index 4c149f0f..f2ce9404 100644 --- a/praxiscore-script/src/main/java/module-info.java +++ b/praxiscore-script/src/main/java/module-info.java @@ -1,13 +1,10 @@ module org.praxislive.script { - requires java.logging; - requires org.praxislive.core; requires org.praxislive.base; exports org.praxislive.script; - exports org.praxislive.script.impl; uses org.praxislive.script.CommandInstaller; diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractSingleCallFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/AbstractSingleCallFrame.java similarity index 59% rename from praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractSingleCallFrame.java rename to praxiscore-script/src/main/java/org/praxislive/script/AbstractSingleCallFrame.java index 6d76e427..715239ac 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractSingleCallFrame.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/AbstractSingleCallFrame.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,33 +19,42 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ -package org.praxislive.script.impl; +package org.praxislive.script; import java.util.List; +import java.util.Objects; import org.praxislive.core.Call; import org.praxislive.core.Value; -import org.praxislive.core.types.PReference; -import org.praxislive.script.Env; -import org.praxislive.script.Namespace; -import org.praxislive.script.StackFrame; +import org.praxislive.core.types.PError; /** - * + * An abstract {@link StackFrame} for commands that need to make a stack frame + * that makes a single call and processes its response. + *

+ * Subclasses should implement + * {@link #createCall(org.praxislive.script.Env, java.util.List)} to create the + * call when required. Subclasses may additionally override + * {@link #processResult(java.util.List)} if the need to alter the return values + * from the call. */ public abstract class AbstractSingleCallFrame implements StackFrame { - private Namespace namespace; - private List args; + private final Namespace namespace; + private final List args; + private State state; private Call call; private List result; protected AbstractSingleCallFrame(Namespace namespace, List args) { - if (namespace == null || args == null) { - throw new NullPointerException(); - } - this.namespace = namespace; - this.args = args; + this.namespace = Objects.requireNonNull(namespace); + this.args = Objects.requireNonNull(args); + state = State.Incomplete; + } + + AbstractSingleCallFrame(List args) { + this.namespace = null; + this.args = Objects.requireNonNull(args); state = State.Incomplete; } @@ -68,7 +77,7 @@ public final StackFrame process(Env env) { } env.getPacketRouter().route(call); } catch (Exception ex) { - result = List.of(PReference.of(ex)); + result = List.of(PError.of(ex)); state = State.Error; } } @@ -81,14 +90,20 @@ public final void postResponse(Call response) { call = null; result = response.args(); if (response.isReply()) { - result = processResult(result); - state = State.OK; + try { + result = processResult(result); + state = State.OK; + } catch (Exception ex) { + result = List.of(PError.of(ex)); + state = State.Error; + } } else { state = State.Error; } } } + @Override public final void postResponse(State state, List args) { throw new IllegalStateException(); } @@ -101,11 +116,27 @@ public final List result() { return result; } + /** + * Create the Call. The call must use {@link Env#getAddress()} as the from + * address, and require a response. + * + * @param env environment for address, time, etc. + * @param args command arguments + * @return call + * @throws Exception on error + */ protected abstract Call createCall(Env env, List args) throws Exception; + /** + * Process the result from the call on a successful response. Unless this + * method is overridden the result of the stack frame will be the result of + * the call. + * + * @param result successful result from call + * @return processed result + */ protected List processResult(List result) { return result; } } - diff --git a/praxiscore-script/src/main/java/org/praxislive/script/Bundle.properties b/praxiscore-script/src/main/java/org/praxislive/script/Bundle.properties deleted file mode 100644 index 36623378..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/Bundle.properties +++ /dev/null @@ -1 +0,0 @@ -OpenIDE-Module-Name=praxis.script diff --git a/praxiscore-script/src/main/java/org/praxislive/script/Command.java b/praxiscore-script/src/main/java/org/praxislive/script/Command.java index 27b05bbb..4e048e46 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/Command.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/Command.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,18 +19,28 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; import java.util.List; import org.praxislive.core.Value; /** - * + * A script command. The script executor will look up commands by name in the + * current {@link Namespace}. Each execution of the command will cause a call + * {@link #createStackFrame(org.praxislive.script.Namespace, java.util.List)}. */ public interface Command { + /** + * Create a StackFrame to execute the command with the provided Namespace + * and arguments. + * + * @param namespace current namespace + * @param args arguments + * @return stack frame to execute command with provided arguments + * @throws Exception if stack frame cannot be created + */ public StackFrame createStackFrame(Namespace namespace, List args) - throws ExecutionException; + throws Exception; } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/CommandInstaller.java b/praxiscore-script/src/main/java/org/praxislive/script/CommandInstaller.java index f5e661ea..0ca7c09b 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/CommandInstaller.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/CommandInstaller.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,17 +19,25 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; import java.util.Map; /** - * - * + * Service provider interface for other modules to provide commands. + *

+ * Implementations should be registered for {@link ServiceLoader} to load. */ public interface CommandInstaller { + /** + * Called on all registered command installers during initialization of a + * script executor. The implementation should add commands to the provided + * map. The String key is the name used to look up the command in the + * {@link Namespace}. A command might be registered under multiple names. + * + * @param commands map to install commands to + */ public void install(Map commands); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/CompoundStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/CompoundStackFrame.java new file mode 100644 index 00000000..d61111ca --- /dev/null +++ b/praxiscore-script/src/main/java/org/praxislive/script/CompoundStackFrame.java @@ -0,0 +1,173 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; +import java.util.function.Function; +import java.util.function.Supplier; +import org.praxislive.core.Call; +import org.praxislive.core.Value; +import org.praxislive.core.types.PError; + +/** + * + */ +final class CompoundStackFrame implements StackFrame { + + private final Queue, StackFrame>> queue; + + private StackFrame active; + private State state; + private List result; + + CompoundStackFrame(StackFrame base, Function, StackFrame> next) { + queue = new ArrayDeque<>(); + queue.add(next); + active = base; + state = State.Incomplete; + } + + @Override + public State getState() { + if (state != State.Incomplete) { + return state; + } else { + return active.getState(); + } + } + + @Override + public void postResponse(Call call) { + active.postResponse(call); + checkActiveState(); + } + + @Override + public void postResponse(State state, List args) { + active.postResponse(state, args); + checkActiveState(); + } + + @Override + public StackFrame process(Env env) { + while (state == State.Incomplete) { + StackFrame frame = active.process(env); + if (frame != null || active.getState() == State.Incomplete) { + return frame; + } + checkActiveState(); + } + return null; + } + + @Override + public List result() { + if (result != null) { + return result; + } else { + throw new IllegalStateException(); + } + } + + void addStage(Function, StackFrame> stage) { + queue.add(stage); + } + + private void checkActiveState() { + switch (active.getState()) { + case Incomplete -> { + } + case OK -> + nextOrComplete(); + default -> { + state = active.getState(); + result = active.result(); + active = null; + queue.clear(); + } + } + } + + private void nextOrComplete() { + if (!queue.isEmpty()) { + try { + active = queue.remove().apply(active.result()); + } catch (Exception ex) { + state = State.Error; + result = List.of(PError.of(ex)); + active = null; + queue.clear(); + } + } else { + state = State.OK; + result = active.result(); + active = null; + } + } + + static class SupplierStackFrame implements StackFrame { + + private final Supplier> supplier; + + private State state = State.Incomplete; + private List result; + + SupplierStackFrame(Supplier> supplier) { + this.supplier = supplier; + } + + @Override + public State getState() { + return state; + } + + @Override + public void postResponse(Call call) { + throw new UnsupportedOperationException(); + } + + @Override + public void postResponse(State state, List args) { + throw new UnsupportedOperationException(); + } + + @Override + public StackFrame process(Env env) { + try { + result = supplier.get(); + state = State.OK; + } catch (Exception ex) { + result = List.of(PError.of(ex)); + state = State.Error; + } + return null; + } + + @Override + public List result() { + return result; + } + } + +} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/ConstantImpl.java b/praxiscore-script/src/main/java/org/praxislive/script/ConstantImpl.java similarity index 69% rename from praxiscore-script/src/main/java/org/praxislive/script/impl/ConstantImpl.java rename to praxiscore-script/src/main/java/org/praxislive/script/ConstantImpl.java index 8d832f93..5e5da477 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/ConstantImpl.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ConstantImpl.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,30 +19,37 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ +package org.praxislive.script; - -package org.praxislive.script.impl; - +import java.util.Objects; import org.praxislive.core.Value; -import org.praxislive.script.Variable; /** + * Default constant implementation used by + * {@link Namespace#createConstant(java.lang.String, org.praxislive.core.Value)}. * - * */ -public class ConstantImpl implements Variable { +final class ConstantImpl implements Variable { - Value value; + private final Value value; public ConstantImpl(Value value) { - this.value = value; + this.value = Objects.requireNonNull(value); } + @Override public void setValue(Value value) { throw new UnsupportedOperationException(); } + @Override public Value getValue() { return value; } + + @Override + public String toString() { + return value.toString(); + } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptServiceImpl.java b/praxiscore-script/src/main/java/org/praxislive/script/DefaultScriptService.java similarity index 87% rename from praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptServiceImpl.java rename to praxiscore-script/src/main/java/org/praxislive/script/DefaultScriptService.java index 99a567ef..991216b7 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptServiceImpl.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/DefaultScriptService.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2019 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,13 +19,11 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ -package org.praxislive.script.impl; +package org.praxislive.script; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; import org.praxislive.base.AbstractRoot; import org.praxislive.core.Call; import org.praxislive.core.Control; @@ -37,22 +35,19 @@ import org.praxislive.core.services.ScriptService; import org.praxislive.core.services.Service; import org.praxislive.core.types.PError; -import org.praxislive.script.Env; /** - * + * A default implementation of {@link ScriptService}. */ -public class ScriptServiceImpl extends AbstractRoot implements RootHub.ServiceProvider { +public final class DefaultScriptService extends AbstractRoot implements RootHub.ServiceProvider { - private static final Logger LOG = Logger.getLogger(ScriptServiceImpl.class.getName()); + private static final System.Logger LOG = System.getLogger(DefaultScriptService.class.getName()); -// private ScriptContext context; -// private ScriptExecutor defaultExecutor; private final Map controls; private final Map contexts; private int exID; - public ScriptServiceImpl() { + public DefaultScriptService() { controls = new HashMap<>(); controls.put(ScriptService.EVAL, new EvalControl()); controls.put(ScriptService.CLEAR, new ClearControl()); @@ -81,7 +76,6 @@ private ScriptExecutor getExecutor(ControlAddress from) { exID++; String id = "_exec_" + exID; EnvImpl env = new EnvImpl(ControlAddress.of(getAddress(), id)); -// ScriptExecutor ex = new ScriptExecutor(env, true); ScriptExecutor ex = new ScriptExecutor(env, from.component()); controls.put(id, new ScriptControl(ex)); contexts.put(from, new ScriptContext(id, ex)); @@ -174,12 +168,12 @@ private EnvImpl(ControlAddress address) { @Override public Lookup getLookup() { - return ScriptServiceImpl.this.getLookup(); + return DefaultScriptService.this.getLookup(); } @Override public long getTime() { - return ScriptServiceImpl.this.getExecutionContext().getTime(); + return DefaultScriptService.this.getExecutionContext().getTime(); } @Override @@ -197,7 +191,8 @@ private class Router implements PacketRouter { @Override public void route(Packet packet) { - LOG.log(Level.FINEST, () -> "Sending Call : ---\n" + packet.toString()); + LOG.log(System.Logger.Level.TRACE, + () -> "Sending Call : ---\n" + packet.toString()); getRouter().route(packet); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/Env.java b/praxiscore-script/src/main/java/org/praxislive/script/Env.java index 0f379b66..62ce696b 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/Env.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/Env.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,28 +19,69 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; +import org.praxislive.core.Clock; import org.praxislive.core.ControlAddress; import org.praxislive.core.Lookup; import org.praxislive.core.PacketRouter; /** + * Environment context interface passed in to + * {@link StackFrame#process(org.praxislive.script.Env)} and + * {@link InlineCommand#process(org.praxislive.script.Env, org.praxislive.script.Namespace, java.util.List)}. + * An implementation of this interface provides access to various services of + * the script executor required to implement commands. * - * */ public interface Env { + /** + * Name of the context variable that relative component, control and port + * addresses are resolved against. Used and controlled by the {@code @} + * command. + */ public final static String CONTEXT = "_CTXT"; + + /** + * Name of the present working directory variable used to resolve relative + * file paths in various commands. + */ public final static String PWD = "_PWD"; - public abstract Lookup getLookup(); + /** + * Lookup object of the script executor. + * + * @return lookup + */ + public Lookup getLookup(); - public abstract long getTime(); + /** + * Current clock time inside the script executor. Should be used when + * creating calls inside a command, and for any other purpose that the + * current clock time is required. + * + * @see Clock + * @return current clock time + */ + public long getTime(); + /** + * A packet router for sending calls during command execution. + * + * @return packet router + */ public abstract PacketRouter getPacketRouter(); + /** + * The control address of this script executor. Should be used as the from + * address in calls created during command execution. Replies to this + * address will be routed to + * {@link StackFrame#postResponse(org.praxislive.core.Call)} on the active + * command. + * + * @return script executor control address + */ public abstract ControlAddress getAddress(); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ExecutionException.java b/praxiscore-script/src/main/java/org/praxislive/script/ExecutionException.java deleted file mode 100644 index c45e4363..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/ExecutionException.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2018 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ - -package org.praxislive.script; - -/** - * - * - */ -public class ExecutionException extends Exception { - - /** - * Creates a new instance of ExecutionException without detail message. - */ - public ExecutionException() { - } - - - /** - * Constructs an instance of ExecutionException with the specified detail message. - * @param msg the detail message. - */ - public ExecutionException(String msg) { - super(msg); - } - - public ExecutionException(Throwable cause) { - super(cause); - } - - public ExecutionException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/InlineCommand.java b/praxiscore-script/src/main/java/org/praxislive/script/InlineCommand.java index f03b02be..6368bddc 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/InlineCommand.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/InlineCommand.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,19 +19,107 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; import java.util.List; +import org.praxislive.core.Call; import org.praxislive.core.Value; +import org.praxislive.core.types.PError; /** - * + * Simple subtype of {@link Command} that can be executed and produce a result + * immediately (without child stack frames or making calls). */ public interface InlineCommand extends Command { + /** + * Execute the command with the given environment, namespace and arguments. + * + * @param context current environment + * @param namespace current namespace + * @param args arguments + * @return result + * @throws Exception on error + */ public List process(Env context, Namespace namespace, List args) - throws ExecutionException; + throws Exception; + + /** + * Create a StackFrame to execute the command with the provided Namespace + * and arguments. + *

+ * The default implementation of this method returns an + * {@link InlineStackFrame} with this command, and provided namespace and + * arguments. Implementations can override to further validate the context + * or delegate to another command. + * + * + * @param namespace current namespace + * @param args arguments + * @return stack frame to execute command with provided arguments + * @throws Exception if stack frame cannot be created + */ + @Override + public default InlineStackFrame createStackFrame(Namespace namespace, List args) throws Exception { + return new InlineStackFrame(this, namespace, args); + } + + /** + * A default implementation of StackFrame for use by InlineCommand + * implementations. + */ + public static final class InlineStackFrame implements StackFrame { + + private final InlineCommand command; + private final Namespace namespace; + private final List args; + private State state; + private List result; + + InlineStackFrame(InlineCommand command, Namespace namespace, List args) { + this.command = command; + this.namespace = namespace; + this.args = args; + state = State.Incomplete; + } + + @Override + public State getState() { + return state; + } + + @Override + public StackFrame process(Env env) { + if (state == State.Incomplete) { + try { + result = command.process(env, namespace, args); + state = State.OK; + } catch (Exception ex) { + result = List.of(PError.of(ex)); + state = State.Error; + } + } + return null; + } + + @Override + public void postResponse(Call call) { + throw new IllegalStateException(); + } + + @Override + public void postResponse(State state, List args) { + throw new IllegalStateException(); + } + + @Override + public List result() { + if (result == null) { + throw new IllegalStateException(); + } + return result; + } + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/Namespace.java b/praxiscore-script/src/main/java/org/praxislive/script/Namespace.java index d58f0a21..0e158765 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/Namespace.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/Namespace.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,23 +19,131 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; +import org.praxislive.core.Value; + /** - * - * + * A Namespace offers storage of {@link Variable} and {@link Command} by name. + * Namespaces exist in a hierarchy. Variables and Commands added to this + * namespace usually shadow those from parent namespaces, and are usually + * visible to child namespaces. + *

+ * A Namespace is passed in from the script executor to + * {@link Command#createStackFrame(org.praxislive.script.Namespace, java.util.List)}. */ public interface Namespace { - public abstract Variable getVariable(String id); + /** + * Get the Variable with the given ID, or {@code null} if it doesn't exist + * in this or a parent namespace. + * + * @param id variable ID + * @return named variable, or null if none exists + */ + public Variable getVariable(String id); + + /** + * Add a Variable with the given ID to this Namespace. + * + * @param id variable ID + * @param var variable to add + * @throws IllegalArgumentException if a variable with that ID already + * exists in this namespace + */ + public void addVariable(String id, Variable var); + + /** + * Get the Command with the given ID, or {@code null} if it doesn't exist in + * this or a parent namespace. + * + * @param id command ID + * @return named command, or null if none exists + */ + public Command getCommand(String id); + + /** + * Add a Command with the given ID to this Namespace. + * + * @param id command ID + * @param cmd command to add + * @throws IllegalArgumentException if a command with that ID already exists + * in this namespace + */ + public void addCommand(String id, Command cmd); - public abstract void addVariable(String id, Variable var); + /** + * Create a child Namespace of this Namespace. + * + * @return child namespace + */ + public Namespace createChild(); - public abstract Command getCommand(String id); + /** + * Create a variable in this namespace with the initial value given. + *

+ * The default implementation of this method creates a new instance of a + * variable implementation, and calls + * {@link #addVariable(java.lang.String, org.praxislive.script.Variable)} to + * register it. + * + * @param id variable ID + * @param value initial value + * @return created variable + * @throws IllegalArgumentException if a variable with that name already + * exists in this namespace + */ + public default Variable createVariable(String id, Value value) { + Variable v = new VariableImpl(value); + addVariable(id, v); + return v; + } - public abstract void addCommand(String id, Command cmd); + /** + * Get the variable with the given ID from this namespace or a parent + * namespace, creating and initializing a variable with the provided default + * value if none exists. + *

+ * The default implementation of this method calls + * {@link #getVariable(java.lang.String)} to find a registered variable, and + * if that method returns {@link null} delegates to + * {@link #createVariable(java.lang.String, org.praxislive.core.Value)}. + * + * @param id variable ID + * @param defaultValue default initial value + * @return created variable + */ + public default Variable getOrCreateVariable(String id, Value defaultValue) { + Variable v = getVariable(id); + if (v == null) { + return createVariable(id, defaultValue); + } else { + return v; + } + } - public abstract Namespace createChild(); + /** + * Create a constant in this namespace with the initial value given. The + * constant is guaranteed to always return {@code value} from + * {@link Variable#getValue()}, and to always throw + * {@link UnsupportedOperationException} on any call to + * {@link Variable#setValue(org.praxislive.core.Value)}. + *

+ * The default implementation of this method creates a new instance of a + * constant variable implementation, and calls + * {@link #addVariable(java.lang.String, org.praxislive.script.Variable)} to + * register it. + * + * @param id constant name + * @param value constant value + * @return created constant + * @throws IllegalArgumentException if a variable with that name already + * exists in this namespace + */ + public default Variable createConstant(String id, Value value) { + Variable c = new ConstantImpl(value); + addVariable(id, c); + return c; + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptExecutor.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java similarity index 71% rename from praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptExecutor.java rename to praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java index 929338ee..f88f0192 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/ScriptExecutor.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,63 +19,49 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ -package org.praxislive.script.impl; +package org.praxislive.script; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; -import java.util.logging.Logger; import org.praxislive.core.Call; import org.praxislive.core.ComponentAddress; import org.praxislive.core.Lookup; import org.praxislive.core.types.PError; -import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; -import org.praxislive.script.Env; -import org.praxislive.script.Namespace; -import org.praxislive.script.StackFrame; -import org.praxislive.script.Variable; import org.praxislive.script.commands.CoreCommandsInstaller; -import org.praxislive.script.commands.ScriptCmds; + +import static java.lang.System.Logger.Level; /** * */ -public class ScriptExecutor { - - private final static Logger log = Logger.getLogger(ScriptExecutor.class.getName()); - private List stack; - private Queue queue; - private Env env; - private Command evaluator; - private Map commandMap; - private Namespace rootNS; - - public ScriptExecutor(Env env, boolean inline) { - this.env = env; - stack = new LinkedList(); - queue = new LinkedList(); - if (inline) { - evaluator = ScriptCmds.INLINE_EVAL; - } else { - evaluator = ScriptCmds.EVAL; - } - rootNS = new NS(); - buildCommandMap(); - } +class ScriptExecutor { - public ScriptExecutor(Env context, final ComponentAddress ctxt) { - this(context, true); + private static final System.Logger log = System.getLogger(ScriptExecutor.class.getName()); + + private final List stack; + private final Queue queue; + private final Env env; + private final Map commandMap; + private final Namespace rootNS; + + ScriptExecutor(Env context, final ComponentAddress ctxt) { + this.env = context; + stack = new LinkedList<>(); + queue = new LinkedList<>(); + commandMap = buildCommandMap(); + rootNS = new NS(); rootNS.addVariable(Env.CONTEXT, new ConstantImpl(ctxt)); } - private void buildCommandMap() { - commandMap = new HashMap(); + private Map buildCommandMap() { + Map map = new HashMap<>(); CommandInstaller installer = new CoreCommandsInstaller(); - installer.install(commandMap); - Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(commandMap)); + installer.install(map); + Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(map)); + return map; } public void queueEvalCall(Call call) { @@ -96,7 +82,7 @@ public void flushEvalQueue() { } public void processScriptCall(Call call) { - log.finest("processScriptCall - received :\n" + call); + log.log(Level.TRACE, () -> "processScriptCall - received :\n" + call); if (!stack.isEmpty()) { stack.get(0).postResponse(call); processStack(); @@ -109,14 +95,14 @@ public void processScriptCall(Call call) { private void processStack() { while (!stack.isEmpty()) { StackFrame current = stack.get(0); - log.finest("Processing stack : " + current.getClass() + log.log(Level.TRACE, () -> "Processing stack : " + current.getClass() + "\n Stack Size : " + stack.size()); // if incomplete do round of processing if (current.getState() == StackFrame.State.Incomplete) { StackFrame child = current.process(env); if (child != null) { - log.finest("Pushing to stack" + child.getClass()); + log.log(Level.TRACE, () -> "Pushing to stack" + child.getClass()); stack.add(0, child); continue; } @@ -128,20 +114,19 @@ private void processStack() { return; } else { var args = current.result(); - log.finest("Stack frame complete : " + current.getClass() + log.log(Level.TRACE, () -> "Stack frame complete : " + current.getClass() + "\n Result : " + args + "\n Stack Size : " + stack.size()); stack.remove(0); if (!stack.isEmpty()) { - log.finest("Posting result up stack"); + log.log(Level.TRACE, "Posting result up stack"); stack.get(0).postResponse(state, args); - continue; } else { Call call = queue.poll(); if (state == StackFrame.State.OK) { - log.finest("Sending OK return call"); + log.log(Level.TRACE, "Sending OK return call"); call = call.reply(args); } else { - log.finest("Sending Error return call"); + log.log(Level.TRACE, "Sending Error return call"); call = call.error(args); } env.getPacketRouter().route(call); @@ -155,7 +140,11 @@ private void checkAndStartEval() { Call call = queue.peek(); var args = call.args(); try { - stack.add(0, evaluator.createStackFrame(rootNS, args)); + var script = args.get(0).toString(); + var stackFrame = ScriptStackFrame.forScript(rootNS, script) + .inline() + .build(); + stack.add(0, stackFrame); processStack(); break; } catch (Exception ex) { @@ -177,9 +166,10 @@ private NS() { private NS(NS parent) { this.parent = parent; - variables = new HashMap(); + variables = new HashMap<>(); } + @Override public Variable getVariable(String id) { Variable var = variables.get(id); if (var == null && parent != null) { @@ -189,6 +179,7 @@ public Variable getVariable(String id) { } } + @Override public void addVariable(String id, Variable var) { if (variables.containsKey(id)) { throw new IllegalArgumentException(); @@ -196,14 +187,17 @@ public void addVariable(String id, Variable var) { variables.put(id, var); } + @Override public Command getCommand(String id) { return commandMap.get(id); } + @Override public void addCommand(String id, Command cmd) { throw new UnsupportedOperationException(); } + @Override public Namespace createChild() { return new NS(this); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java new file mode 100644 index 00000000..62970469 --- /dev/null +++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java @@ -0,0 +1,439 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.praxislive.core.Value; +import org.praxislive.core.Call; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.syntax.InvalidSyntaxException; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PError; +import org.praxislive.core.types.PString; +import org.praxislive.script.ast.RootNode; +import org.praxislive.script.ast.ScriptParser; + +import static java.lang.System.Logger.Level; + +/** + * A stackframe implementation that supports parsing and running of Pcl scripts. + */ +public final class ScriptStackFrame implements StackFrame { + + private static final String TRAP = "_TRAP"; + + private static final System.Logger log = System.getLogger(ScriptStackFrame.class.getName()); + + private final Namespace namespace; + private final RootNode rootNode; + private final boolean trapErrors; + private final List scratchList; + + private State state; + private String activeCommand; + private Call pending; + private List result; + private boolean doProcess; + + private ScriptStackFrame(Namespace namespace, + RootNode rootNode, + boolean trapErrors) { + this.namespace = namespace; + this.rootNode = rootNode; + this.state = State.Incomplete; + this.trapErrors = trapErrors; + this.scratchList = new ArrayList<>(); + rootNode.reset(); + if (trapErrors) { + namespace.createVariable(TRAP, PArray.EMPTY); + } + rootNode.init(namespace); + doProcess = true; + } + + @Override + public State getState() { + return state; + } + + @Override + public StackFrame process(Env context) { + if (state != State.Incomplete) { + throw new IllegalStateException(); + } + if (!doProcess) { + return null; + } + while (!rootNode.isDone() && state == State.Incomplete) { + try { + return processNextCommand(context); + } catch (Exception ex) { + postError(List.of(PError.of(ex))); + } + } + if (rootNode.isDone() && state == State.Incomplete) { + + try { + processResultFromNode(); + } catch (Exception ex) { + result = List.of(PError.of(ex)); + state = State.Error; + } + } + return null; + } + + @Override + public void postResponse(Call call) { + if (pending != null && pending.matchID() == call.matchID()) { + pending = null; + if (call.isReply()) { + log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Return call : \n" + call); + postResponse(call.args()); + } else { + log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Error call : \n" + call); + postError(call.args()); + } + doProcess = true; + } else { + log.log(Level.TRACE, () -> "EvalStackFrame - Received invalid call : \n" + call); + } + + } + + @Override + public void postResponse(State state, List args) { + if (this.state != State.Incomplete) { + throw new IllegalStateException(); + } + switch (state) { + case Incomplete -> + throw new IllegalArgumentException(); + case OK -> + postResponse(args); + case Error -> + postError(args); + default -> { + this.state = state; + this.result = List.copyOf(args); + } + } + doProcess = true; + } + + @Override + public List result() { + if (state == State.Incomplete) { + throw new IllegalStateException(); + } + if (result == null) { + return List.of(); + } else { + return result; + } + } + + private void postResponse(List args) { + try { + scratchList.clear(); + scratchList.addAll(args); + rootNode.postResponse(scratchList); + } catch (Exception ex) { + state = State.Error;//@TODO proper error reporting + } + } + + private void postError(List args) { + Variable trap = namespace.getVariable(TRAP); + if (trap != null) { + rootNode.skipCurrentLine(); + PArray existing = PArray.from(trap.getValue()).orElse(PArray.EMPTY); + Value response = args.isEmpty() ? null : args.getFirst(); + trap.setValue(addErrorToTrap(existing, response)); + } else { + result = List.copyOf(args); + state = State.Error; + } + } + + private PArray addErrorToTrap(PArray trap, Value response) { + String msg; + if (response == null) { + msg = activeCommand + " : Error"; + } else { + msg = PError.from(response) + .map(err -> activeCommand + " : " + err.exceptionType().getSimpleName() + + " : " + err.message()) + .orElse(activeCommand + " : Error : " + response); + } + return Stream.concat(trap.stream(), Stream.of(PString.of(msg))) + .collect(PArray.collector()); + } + + private void processResultFromNode() throws Exception { + scratchList.clear(); + Variable trap = namespace.getVariable(TRAP); + if (trapErrors && trap != null && !trap.getValue().isEmpty()) { + String errors = PArray.from(trap.getValue()) + .orElse(PArray.EMPTY) + .asListOf(String.class) + .stream() + .collect(Collectors.joining("\n")); + result = List.of(PString.of(errors)); + state = State.Error; + } else { + rootNode.writeResult(scratchList); + result = List.copyOf(scratchList); + state = State.OK; + } + } + + private StackFrame processNextCommand(Env context) + throws Exception { + + scratchList.clear(); + rootNode.writeNextCommand(scratchList); + if (scratchList.size() < 1) { + throw new Exception(); + } + Value cmdArg = scratchList.get(0); + activeCommand = cmdArg.toString(); + if (cmdArg instanceof ControlAddress) { + routeCall(context, scratchList); + return null; + } + String cmdStr = cmdArg.toString(); + if (cmdStr.isEmpty()) { + throw new IllegalArgumentException("Empty command"); + } + Command cmd = namespace.getCommand(cmdStr); + if (cmd != null) { + scratchList.remove(0); + return cmd.createStackFrame(namespace, List.copyOf(scratchList)); + } + if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) { + routeCall(context, scratchList); + return null; + } + + throw new IllegalArgumentException("Command not found"); + + } + + private void routeCall(Env context, List argList) + throws Exception { + ControlAddress ad = ControlAddress.from(argList.get(0)) + .orElseThrow(Exception::new); + argList.remove(0); + Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList)); + log.log(Level.TRACE, () -> "Sending Call" + call); + pending = call; + context.getPacketRouter().route(call); + } + + /** + * Create a {@link ScriptStackFrame.Builder} for the provided namespace and + * script. By default the script will be evaluated in a dedicated child + * namespace. Neither the builder or the stack frame are reusable. + * + * @param namespace namespace to run script in + * @param script script to parse and run + * @return builder + * @throws InvalidSyntaxException if the script cannot be parsed + */ + public static Builder forScript(Namespace namespace, String script) { + RootNode root = ScriptParser.getInstance().parse(script); + return new Builder(namespace, root); + } + + /** + * A builder for {@link ScriptStackFrame}. + * + * @see #forScript(org.praxislive.script.Namespace, java.lang.String) + */ + public static class Builder { + + private final Namespace namespace; + private final RootNode root; + + private boolean inline; + private List allowedCommands; + private boolean trapErrors; + private List> namespaceProcessors; + + private Builder(Namespace namespace, RootNode root) { + this.namespace = namespace; + this.root = root; + } + + /** + * Run the script directly in the provided namespace rather than a + * child. + * + * @return this for chaining + */ + public Builder inline() { + if (trapErrors) { + throw new IllegalStateException("Inline and trap errors cannot be used together"); + } + this.inline = true; + return this; + } + + /** + * Trap errors. Error messages will be aggregated and script execution + * will attempt to continue. If no allowed commands have been specified, + * an empty list of allowed commands will be set. + * + * @return this for chaining + */ + public Builder trapErrors() { + if (inline) { + throw new IllegalStateException("Inline and trap errors cannot be used together"); + } + this.trapErrors = true; + if (this.allowedCommands == null) { + this.allowedCommands = List.of(); + } + return this; + } + + /** + * Specify a list of allowed commands to filter those available from the + * provided namespace. + * + * @param commands list of allowed commands + * @return this for chaining + */ + public Builder allowedCommands(List commands) { + this.allowedCommands = List.copyOf(commands); + return this; + } + + /** + * Create a constant with the given name and value in the script + * namespace. + * + * @param id constant name + * @param value constant value + * @return this for chaining + */ + public Builder createConstant(String id, Value value) { + addNamespaceProcessor(ns -> ns.createConstant(id, value)); + return this; + } + + /** + * Create a variable with the given name and value in the script + * namespace. + * + * @param id variable name + * @param value variable value + * @return this for chaining + */ + public Builder createVariable(String id, Value value) { + addNamespaceProcessor(ns -> ns.createVariable(id, value)); + return this; + } + + private void addNamespaceProcessor(Consumer processor) { + if (namespaceProcessors == null) { + namespaceProcessors = new ArrayList<>(); + } + namespaceProcessors.add(processor); + } + + /** + * Build the ScriptStackFrame. + * + * @return script stackframe + */ + public ScriptStackFrame build() { + Namespace ns; + if (inline) { + ns = namespace; + } else { + ns = namespace.createChild(); + } + if (namespaceProcessors != null) { + Namespace nsp = ns; + namespaceProcessors.forEach(p -> p.accept(nsp)); + } + if (allowedCommands != null) { + ns = new FilteredNamespace(ns, allowedCommands); + } + return new ScriptStackFrame(ns, root, trapErrors); + } + + } + + private static class FilteredNamespace implements Namespace { + + private final Namespace delegate; + private final List allowed; + + private FilteredNamespace(Namespace delegate, List allowed) { + this.delegate = Objects.requireNonNull(delegate); + this.allowed = List.copyOf(allowed); + } + + @Override + public void addCommand(String id, Command cmd) { + if (allowed.contains(id)) { + delegate.addCommand(id, cmd); + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public void addVariable(String id, Variable var) { + delegate.addVariable(id, var); + } + + @Override + public Namespace createChild() { + return new FilteredNamespace(delegate.createChild(), allowed); + } + + @Override + public Command getCommand(String id) { + if (allowed.contains(id)) { + return delegate.getCommand(id); + } else { + return null; + } + } + + @Override + public Variable getVariable(String id) { + return delegate.getVariable(id); + } + + } + +} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/StackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/StackFrame.java index 202f52bc..50cca8d8 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/StackFrame.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/StackFrame.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,29 +19,256 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Function; +import java.util.function.UnaryOperator; import org.praxislive.core.Call; +import org.praxislive.core.ControlAddress; import org.praxislive.core.Value; +import org.praxislive.core.services.Service; +import org.praxislive.core.services.ServiceUnavailableException; +import org.praxislive.core.services.Services; +import org.praxislive.core.services.TaskService; +import org.praxislive.core.types.PReference; /** - * + * A StackFrame used within the script executor pointing to the currently + * executing command. A StackFrame is created for each execution of a Command + * using + * {@link Command#createStackFrame(org.praxislive.script.Namespace, java.util.List)}. + *

+ * A StackFrame should always start off in {@link State#Incomplete}. The script + * executor will call {@link #process(org.praxislive.script.Env)}. During + * processing the StackFrame may evaluate a result, make one or more Calls, or + * create a child StackFrame (eg. from evaluation of another command). + *

+ * If a Call has been made, the state should remain incomplete. Any returning + * call will be passed into {@link #postResponse(org.praxislive.core.Call)}. + *

+ * If a child StackFrame has been returned, the result of its processing will be + * passed into + * {@link #postResponse(org.praxislive.script.StackFrame.State, java.util.List)}. + *

+ * Once a response has been posted, the script executor will check if the + * StackFrame is still marked incomplete. If it is still incomplete, the + * executor will call {@link #process(org.praxislive.script.Env)} again. If it + * has any other state, the state and result will be posted up to the parent + * StackFrame if there is one, or returned as the script result. */ public interface StackFrame { - public static enum State {Incomplete, OK, Error, Break, Continue}; + /** + * Possible states of a StackFrame. All StackFrames start in an incomplete + * state. + */ + public static enum State { + + /** + * Incomplete and requires processing. All StackFrames begin in this + * state. + */ + Incomplete, + /** + * Processing finished successfully, and the {@link #result()} is + * available. + */ + OK, + /** + * Processing finished with an error. + */ + Error, + /** + * Special state to control stack unwinding. + * + */ + Break, + /** + * Special state to control stack unwinding. + */ + Continue + }; + /** + * Get the current state of this StackFrame. + * + * @return current state + */ public State getState(); + /** + * Process the StackFrame. After processing, the StackFrame should have made + * one or more Calls, returned a child StackFrame, or moved out of the + * Incomplete state. + *

+ * Process may be called multiple times if the state is still incomplete + * after this method returns and a response has been posted. + * + * @param env processing environment + * @return child StackFrame or null + */ public StackFrame process(Env env); + /** + * Used by the script executor to post the result of a Call. The StackFrame + * should validate the match ID of the response call against any pending + * calls before processing the call state or arguments. + *

+ * If the state is still incomplete after a response is posted, + * {@link #process(org.praxislive.script.Env)} will be called again. + * + * @param call response call + * @throws IllegalStateException if the state is not incomplete or a call + * response is not expected + */ public void postResponse(Call call); + /** + * Used by the script executor to post the result of a child StackFrame + * returned by {@link #process(org.praxislive.script.Env)}. + *

+ * If the state is still incomplete after a response is posted, + * {@link #process(org.praxislive.script.Env)} will be called again. + * + * @param state the completion state of the child stack frame + * @param args the result of the child stack frame + * @throws IllegalStateException if the state is not incomplete or a child + * stack frame result is not expected + */ public void postResponse(State state, List args); - + + /** + * Access the result of this StackFrame. + * + * @return result + * @throws IllegalStateException if the state is incomplete + */ public List result(); + /** + * Combine this StackFrame with another created from the result of this + * StackFrame. The returned StackFrame will execute the frames in turn. + *

+ * The default implementation returns a private implementation of a compound + * stackframe. If this method is called on an existing compound stack frame, + * then the stage function will be added to that and {@code this} will be + * returned. + * + * @param stage function to create next stack frame from result + * @return compound stackframe + */ + public default StackFrame andThen(Function, StackFrame> stage) { + if (this instanceof CompoundStackFrame csf) { + csf.addStage(stage); + return this; + } else { + return new CompoundStackFrame(this, stage); + } + } + + /** + * Map the result of this StackFrame with the provided mapping function + * before returning a result or using + * {@link #andThen(java.util.function.Function)}. + *

+ * The default implementation calls + * {@link #andThen(java.util.function.Function)} with a function that + * creates a private implementation of a mapping StackFrame. + * + * @param mapper map value list + * @return mapping stackframe + */ + public default StackFrame andThenMap(UnaryOperator> mapper) { + return andThen(args -> new CompoundStackFrame.SupplierStackFrame( + () -> mapper.apply(args)) + ); + } + + /** + * Create a StackFrame that makes a call to the provided control and returns + * the result. + * + * @param to control address + * @param arg single argument + * @return stackframe + */ + public static StackFrame call(ControlAddress to, Value arg) { + return call(to, List.of(arg)); + } + + /** + * Create a StackFrame that makes a call to the provided control and returns + * the result. + * + * @param to control address + * @param args arguments + * @return stackframe + */ + public static StackFrame call(ControlAddress to, List args) { + return new AbstractSingleCallFrame(args) { + @Override + protected Call createCall(Env env, List args) throws Exception { + return Call.create(to, env.getAddress(), env.getTime(), args); + } + }; + } + + /** + * Create a StackFrame that makes a call to the provided {@link Service} and + * returns the result. The first implementation of the service found in the + * Env lookup will be used. + * + * @param service type of service + * @param control id of control on service + * @param arg single argument + * @return stackframe + * @throws ServiceUnavailableException if no implementation of the service + * is found + * + */ + public static StackFrame serviceCall(Class service, + String control, Value arg) { + return serviceCall(service, control, List.of(arg)); + } + + /** + * Create a StackFrame that makes a call to the provided {@link Service} and + * returns the result. The first implementation of the service found in the + * Env lookup will be used. + * + * @param service type of service + * @param control id of control on service + * @param args arguments + * @return stackframe + * @throws ServiceUnavailableException if no implementation of the service + * is found + */ + public static StackFrame serviceCall(Class service, + String control, List args) { + return new AbstractSingleCallFrame(args) { + @Override + protected Call createCall(Env env, List args) throws Exception { + ControlAddress to = ControlAddress.of( + env.getLookup().find(Services.class) + .flatMap(sm -> sm.locate(service)) + .orElseThrow(ServiceUnavailableException::new), + control + ); + return Call.create(to, env.getAddress(), env.getTime(), args); + } + }; + } + + /** + * Create a StackFrame that executes the provided task asynchronously in the + * default {@link TaskService} and returns the result. + * + * @param task task to execute + * @return stackframe + */ + public static StackFrame async(TaskService.Task task) { + return serviceCall(TaskService.class, TaskService.SUBMIT, PReference.of(task)); + } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/Variable.java b/praxiscore-script/src/main/java/org/praxislive/script/Variable.java index 682af123..56d987e2 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/Variable.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/Variable.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,19 +19,34 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script; import org.praxislive.core.Value; /** + * Storage for a value, to be used with {@link Namespace}. Variable + * implementations might be read-only, only settable at certain times, or + * validate their values. * - * */ public interface Variable { - public abstract void setValue(Value value); + /** + * Set the value of this variable. A variable may not be settable + * (read-only) or may me only settable at certain times. A variable might + * validate its value, eg. a particular type, range, etc. + * + * @param value new value, not null + * @throws UnsupportedOperationException if the value cannot be set + * @throws IllegalArgumentException if the value is not valid + */ + public void setValue(Value value); - public abstract Value getValue(); + /** + * Get the current value of the variable. + * + * @return current value + */ + public Value getValue(); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/VariableImpl.java b/praxiscore-script/src/main/java/org/praxislive/script/VariableImpl.java similarity index 66% rename from praxiscore-script/src/main/java/org/praxislive/script/impl/VariableImpl.java rename to praxiscore-script/src/main/java/org/praxislive/script/VariableImpl.java index df2314c1..346ee6de 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/VariableImpl.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/VariableImpl.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,30 +19,37 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ +package org.praxislive.script; - -package org.praxislive.script.impl; - +import java.util.Objects; import org.praxislive.core.Value; -import org.praxislive.script.Variable; /** + * Default variable implementation used by + * {@link Namespace#createVariable(java.lang.String, org.praxislive.core.Value)}. * - * */ -public class VariableImpl implements Variable { +final class VariableImpl implements Variable { - Value value; + private Value value; public VariableImpl(Value value) { - this.value = value; + this.value = Objects.requireNonNull(value); } + @Override public void setValue(Value value) { - this.value = value; + this.value = Objects.requireNonNull(value); } + @Override public Value getValue() { return value; } + + @Override + public String toString() { + return value.toString(); + } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java index 8429f7d2..ec0267ba 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,29 +19,28 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.ast; import java.util.List; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; /** * - * + * */ -public abstract class CompositeNode extends Node { - - private Node[] children; +abstract class CompositeNode extends Node { + + private final List children; + private int active; private Namespace namespace; public CompositeNode(List children) { - this.children = children.toArray(new Node[children.size()]); + this.children = List.copyOf(children); } - @Override + @Override public void init(Namespace namespace) { if (namespace == null) { throw new NullPointerException(); @@ -51,9 +50,7 @@ public void init(Namespace namespace) { } this.namespace = namespace; active = 0; - for (Node child : children) { - child.init(namespace); - } + children.forEach(child -> child.init(namespace)); } @Override @@ -64,8 +61,8 @@ public boolean isDone() { if (active < 0) { return isThisDone(); } else { - while (active < children.length) { - if (!children[active].isDone()) { + while (active < children.size()) { + if (!children.get(active).isDone()) { return false; } active++; @@ -78,37 +75,36 @@ public boolean isDone() { protected abstract boolean isThisDone(); @Override - public void writeNextCommand(List args) - throws ExecutionException { + public void writeNextCommand(List args) + throws Exception { if (namespace == null) { throw new IllegalStateException(); } if (active >= 0) { - children[active].writeNextCommand(args); + children.get(active).writeNextCommand(args); } else { writeThisNextCommand(args); } } protected abstract void writeThisNextCommand(List args) - throws ExecutionException; - + throws Exception; @Override - public void postResponse(List args) - throws ExecutionException { + public void postResponse(List args) + throws Exception { if (namespace == null) { throw new IllegalStateException(); } if (active >= 0) { - children[active].postResponse(args); + children.get(active).postResponse(args); } else { postThisResponse(args); } } protected abstract void postThisResponse(List args) - throws ExecutionException; + throws Exception; @Override public void reset() { @@ -117,11 +113,19 @@ public void reset() { } namespace = null; } - - protected Node[] getChildren() { + + protected List getChildren() { return children; } - + protected void skipActive() { + if (active < 0) { + return; + } + active++; + if (active >= children.size()) { + active = -1; + } + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/LineNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/LineNode.java index 61e90128..99d20561 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/LineNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/LineNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -22,18 +22,14 @@ package org.praxislive.script.ast; import java.util.List; -import java.util.logging.Logger; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; /** * - * + * */ public class LineNode extends CompositeNode { - private final static Logger log = Logger.getLogger(LineNode.class.getName()); - private Value[] result; public LineNode(List children) { @@ -46,13 +42,12 @@ protected boolean isThisDone() { } @Override - protected void writeThisNextCommand(List args) - throws ExecutionException { + protected void writeThisNextCommand(List args) + throws Exception { if (result == null) { for (Node child : getChildren()) { child.writeResult(args); } - log.finest("LineNode writing command : " + args.toString()); } else { throw new IllegalStateException(); } @@ -62,7 +57,7 @@ protected void writeThisNextCommand(List args) @Override protected void postThisResponse(List args) { if (result == null) { - result = args.toArray(new Value[args.size()]); + result = args.toArray(Value[]::new); } else { throw new IllegalStateException(); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/Node.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/Node.java index a896bf00..8a96f691 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/Node.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/Node.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -23,7 +23,6 @@ import java.util.List; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; /** @@ -40,17 +39,17 @@ public boolean isDone() { } public void writeNextCommand(List args) - throws ExecutionException { - throw new ExecutionException(); + throws Exception { + throw new Exception(); } public void postResponse(List args) - throws ExecutionException { - throw new ExecutionException(); + throws Exception { + throw new Exception(); } public abstract void writeResult(List args) - throws ExecutionException; + throws Exception; public void reset() { } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java index 72c0ffcd..794a6f2e 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,16 +19,14 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.ast; import java.util.List; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; /** * - * + * */ public class RootNode extends CompositeNode { @@ -52,14 +50,18 @@ protected void postThisResponse(List args) { } @Override - public void writeResult(List args) throws ExecutionException { - Node[] children = getChildren(); - if (children.length > 0) { - children[children.length - 1].writeResult(args); + public void writeResult(List args) throws Exception { + List children = getChildren(); + if (!children.isEmpty()) { + Node last = children.getLast(); + if (last.isDone()) { + last.writeResult(args); + } } - } - + public void skipCurrentLine() { + skipActive(); + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java index d0620029..0c2829c6 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -23,11 +23,10 @@ import java.util.List; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; /** * - * + * */ public class SubcommandNode extends CompositeNode { @@ -52,8 +51,8 @@ protected void postThisResponse(List args) { @Override public void writeResult(List args) - throws ExecutionException { - Node[] children = getChildren(); - children[children.length - 1].writeResult(args); + throws Exception { + List children = getChildren(); + children.get(children.size() - 1).writeResult(args); } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/VariableNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/VariableNode.java index f3e09e6a..d6d80b7c 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/VariableNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/VariableNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,24 +19,19 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.ast; import java.util.List; -import java.util.logging.Logger; import org.praxislive.core.Value; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; import org.praxislive.script.Variable; /** * - * + * */ public class VariableNode extends Node { - private final static Logger log = Logger.getLogger(VariableNode.class.getName()); - private String id; private Namespace namespace; @@ -45,7 +40,6 @@ public VariableNode(String id) { throw new NullPointerException(); } this.id = id; - log.finest("Created VariableNode with id : " + id); } @Override @@ -61,18 +55,15 @@ public void reset() { } @Override - public void writeResult(List args) throws ExecutionException { + public void writeResult(List args) throws Exception { if (namespace == null) { throw new IllegalStateException(); } Variable var = namespace.getVariable(id); if (var == null) { - log.finest("VARIABLE NODE : Can't find variable " + id + " in namespace " + namespace); - throw new ExecutionException(); + throw new Exception("Can't find variable " + id + " in namespace " + namespace); } args.add(var.getValue()); } - - } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ArrayCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ArrayCmds.java index fd9b8a37..76cae971 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ArrayCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/ArrayCmds.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,47 +19,135 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.commands; import java.util.List; -import org.praxislive.script.impl.AbstractInlineCommand; import java.util.Map; import org.praxislive.core.Value; import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PNumber; import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; +import org.praxislive.script.InlineCommand; import org.praxislive.script.Namespace; /** * */ -public class ArrayCmds implements CommandInstaller { - - private final static ArrayCmds INSTANCE = new ArrayCmds(); - - private final static Array ARRAY = new Array(); +class ArrayCmds { - private ArrayCmds() {} + private static final Array ARRAY = new Array(); + private static final ArrayGet ARRAY_GET = new ArrayGet(); + private static final ArrayJoin ARRAY_JOIN = new ArrayJoin(); + private static final ArrayRange ARRAY_RANGE = new ArrayRange(); + private static final ArraySize ARRAY_SIZE = new ArraySize(); - public void install(Map commands) { - commands.put("array", ARRAY); + private ArrayCmds() { } - public final static ArrayCmds getInstance() { - return INSTANCE; + static void install(Map commands) { + commands.put("array", ARRAY); + commands.put("array-get", ARRAY_GET); + commands.put("array-join", ARRAY_JOIN); + commands.put("array-range", ARRAY_RANGE); + commands.put("array-size", ARRAY_SIZE); } - private static class Array extends AbstractInlineCommand { + private static class Array implements InlineCommand { @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.isEmpty()) { + return List.of(PArray.EMPTY); + } PArray ar = args.stream().collect(PArray.collector()); return List.of(ar); } } + private static class ArrayGet implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 2) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PArray array = PArray.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("First argument is not an array")); + + int index = PNumber + .from(args.get(1)) + .orElseThrow(() -> new IllegalArgumentException("Second argument is not a number")) + .toIntValue(); + + return List.of(array.get(index)); + } + + } + + private static class ArrayJoin implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + PArray result = args.stream() + .flatMap(v -> PArray.from(v).stream()) + .flatMap(PArray::stream) + .collect(PArray.collector()); + return List.of(result); + } + + } + + private static class ArrayRange implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() < 2 || args.size() > 3) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PArray array = PArray.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("First argument is not an array")); + + int from, to; + if (args.size() == 2) { + from = 0; + to = PNumber + .from(args.get(1)) + .orElseThrow(() -> new IllegalArgumentException("Second argument is not a number")) + .toIntValue(); + } else { + from = PNumber + .from(args.get(1)) + .orElseThrow(() -> new IllegalArgumentException("Second argument is not a number")) + .toIntValue(); + to = PNumber + .from(args.get(2)) + .orElseThrow(() -> new IllegalArgumentException("Third argument is not a number")) + .toIntValue(); + } + + return List.of(PArray.of(array.asList().subList(from, to))); + } + + } + + private static class ArraySize implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 1) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PArray array = PArray.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("Argument is not an array")); + + return List.of(PNumber.of(array.size())); + } + + } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java index 03b3e4c7..8e2435f2 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -22,237 +22,123 @@ package org.praxislive.script.commands; import java.util.List; -import org.praxislive.script.impl.AbstractSingleCallFrame; -import org.praxislive.script.impl.VariableImpl; import java.util.Map; +import java.util.function.Function; import org.praxislive.core.Value; -import org.praxislive.core.ValueFormatException; -import org.praxislive.core.Call; import org.praxislive.core.ComponentAddress; import org.praxislive.core.ComponentType; import org.praxislive.core.ControlAddress; import org.praxislive.core.protocols.ContainerProtocol; import org.praxislive.core.services.RootManagerService; -import org.praxislive.core.services.ServiceUnavailableException; -import org.praxislive.core.services.Services; -import org.praxislive.core.types.PError; -import org.praxislive.core.types.PReference; import org.praxislive.core.types.PString; import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; +import org.praxislive.script.ScriptStackFrame; import org.praxislive.script.StackFrame; /** * */ -public class AtCmds implements CommandInstaller { +class AtCmds { - private final static AtCmds INSTANCE = new AtCmds(); private final static At AT = new At(); private final static NotAt NOT_AT = new NotAt(); private AtCmds() { } - @Override - public void install(Map commands) { + static void install(Map commands) { commands.put("@", AT); commands.put("!@", NOT_AT); } - public final static AtCmds getInstance() { - return INSTANCE; - } - private static class At implements Command { @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { - if (args.size() < 2) { - throw new ExecutionException(); + if (args.size() < 2 || args.size() > 3) { + throw new IllegalArgumentException("Incorrect number of arguments"); } - try { - ComponentAddress ctxt = ComponentAddress.from(args.get(0)) + ComponentAddress ctxt = ComponentAddress.from(args.get(0)) + .orElseThrow(IllegalArgumentException::new); + ComponentType type; + String script; + if (args.size() == 3) { + type = ComponentType.from(args.get(1)) .orElseThrow(IllegalArgumentException::new); - if (args.size() == 3) { - ComponentType type = ComponentType.from(args.get(1)) - .orElseThrow(IllegalArgumentException::new); - return new AtStackFrame(namespace, ctxt, type, args.get(2)); - } else { - Value arg = args.get(1); - if (! arg.toString().contains(" ")) { - try { - ComponentType type = ComponentType.from(arg).get(); - return new AtStackFrame(namespace, ctxt, type, PString.EMPTY); - } catch (Exception ex) { - // fall through - } - } - return new AtStackFrame(namespace, ctxt, null, arg); - } - } catch (Exception ex) { - throw new ExecutionException(ex); - } - - } - } - - private static class NotAt implements Command { - - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { - return new NotAtStackFrame(namespace, args); - } - - } - - private static class AtStackFrame implements StackFrame { - - private State state; - private final Namespace namespace; - private final ComponentAddress ctxt; - private final ComponentType type; - private final Value script; - private int stage; - private List result; - private Call active; - - private AtStackFrame(Namespace namespace, ComponentAddress ctxt, - ComponentType type, Value script) { - this.namespace = namespace; - this.ctxt = ctxt; - this.type = type; - this.script = script; - state = State.Incomplete; - if (type == null) { - stage = 2; + script = args.get(2).toString(); } else { - stage = 0; - } - } - - @Override - public State getState() { - return state; - } - - @Override - public StackFrame process(Env env) { - if (stage == 0) { - stage++; - try { - - ControlAddress to; - List args; - int depth = ctxt.depth(); - if (depth == 1) { - to = ControlAddress.of( - env.getLookup().find(Services.class) - .flatMap(sm -> sm.locate(RootManagerService.class)) - .orElseThrow(ServiceUnavailableException::new), - RootManagerService.ADD_ROOT); - args = List.of(PString.of(ctxt.rootID()), type); - } else { - to = ControlAddress.of(ctxt.parent(), - ContainerProtocol.ADD_CHILD); - args = List.of(PString.of(ctxt.componentID(depth - 1)), type); + Value arg = args.get(1); + if (!arg.toString().contains(" ")) { + try { + type = ComponentType.from(arg).get(); + script = null; + } catch (Exception ex) { + type = null; + script = arg.toString(); } - active = Call.create(to, env.getAddress(), env.getTime(), args); - env.getPacketRouter().route(active); - - } catch (Exception ex) { - state = State.Error; - result = List.of(PError.of(ex)); + } else { + type = null; + script = arg.toString(); } } - if (stage == 2) { - stage++; - try { - Namespace child = namespace.createChild(); - child.addVariable(Env.CONTEXT, new VariableImpl(ctxt)); - return ScriptCmds.INLINE_EVAL.createStackFrame(child, List.of(script)); - } catch (Exception ex) { - state = State.Error; - result = List.of(PError.of(ex)); + StackFrame create = null; + if (type != null) { + if (ctxt.depth() == 1) { + create = StackFrame.serviceCall(RootManagerService.class, RootManagerService.ADD_ROOT, + List.of(PString.of(ctxt.rootID()), type)); + } else { + create = StackFrame.call( + ControlAddress.of(ctxt.parent(), ContainerProtocol.ADD_CHILD), + List.of(PString.of(ctxt.componentID()), type)); } } + Function, StackFrame> eval = null; + if (script != null) { + String s = script; + eval = v -> { + return ScriptStackFrame.forScript(namespace, s) + .createConstant(Env.CONTEXT, ctxt) + .build(); + }; - return null; - } - - @Override - public void postResponse(Call call) { - if (active != null && call.matchID() == active.matchID()) { - active = null; - if (call.isReply() && stage == 1) { - stage++; - } else { - result = call.args(); - this.state = State.Error; - } } - } - @Override - public void postResponse(State state, List args) { - if (state == State.OK) { -// if (stage == 1) { -// stage++; -// } else - if (stage == 3) { - this.state = State.OK; - result = args; + if (create != null) { + if (eval != null) { + return create.andThen(eval); + } else { + return create; } + } else if (eval != null) { + return eval.apply(List.of()); } else { - this.state = state; - result = args; - } - } - - @Override - public List result() { - if (result == null) { throw new IllegalStateException(); } - return result; + } } - private static class NotAtStackFrame extends AbstractSingleCallFrame { - - private NotAtStackFrame(Namespace ns, List args) { - super(ns, args); - } + private static class NotAt implements Command { @Override - protected Call createCall(Env env, List args) throws Exception { - ComponentAddress comp = ComponentAddress.from(args.get(0)) + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { + ComponentAddress component = ComponentAddress.from(args.get(0)) .orElseThrow(IllegalArgumentException::new); - if (comp.depth() == 1) { - return createRootRemovalCall(env, comp.rootID()); + if (component.depth() == 1) { + return StackFrame.serviceCall(RootManagerService.class, + RootManagerService.REMOVE_ROOT, + PString.of(component.componentID())); } else { - return createChildRemovalCall(env, comp); + return StackFrame.call(ControlAddress.of(component.parent(), + ContainerProtocol.REMOVE_CHILD), + PString.of(component.componentID())); } } - private Call createRootRemovalCall(Env env, String id) throws Exception { - ControlAddress to = ControlAddress.of( - env.getLookup().find(Services.class) - .flatMap(sm -> sm.locate(RootManagerService.class)) - .orElseThrow(ServiceUnavailableException::new), - RootManagerService.REMOVE_ROOT); - return Call.create(to, env.getAddress(), env.getTime(), PString.of(id)); - } - - private Call createChildRemovalCall(Env env, ComponentAddress comp) throws Exception { - ControlAddress to = ControlAddress.of(comp.parent(), - ContainerProtocol.REMOVE_CHILD); - return Call.create(to, env.getAddress(), env.getTime(), - PString.of(comp.componentID(comp.depth() - 1))); - } } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/BaseCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/BaseCmds.java new file mode 100644 index 00000000..2c659376 --- /dev/null +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/BaseCmds.java @@ -0,0 +1,122 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script.commands; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.praxislive.core.Value; +import org.praxislive.core.types.PString; +import org.praxislive.script.Command; +import org.praxislive.script.Env; +import org.praxislive.script.InlineCommand; +import org.praxislive.script.Namespace; +import org.praxislive.script.Variable; + +import static java.lang.System.Logger.Level; + +/** + * + */ +class BaseCmds { + + private static final System.Logger LOG = System.getLogger(BaseCmds.class.getName()); + + private static final Constant CONSTANT = new Constant(); + private static final Set SET = new Set(); + private static final Var VAR = new Var(); + private static final Echo ECHO = new Echo(); + + private BaseCmds() { + } + + static void install(Map commands) { + commands.put("constant", CONSTANT); + commands.put("set", SET); + commands.put("var", VAR); + commands.put("echo", ECHO); + } + + private static class Set implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 2) { + throw new Exception(); + } + String varName = args.get(0).toString(); + Value val = args.get(1); + Variable var = namespace.getVariable(varName); + if (var != null) { + var.setValue(val); + } else { + LOG.log(Level.TRACE, () -> "SET COMMAND : Adding variable " + varName + " to namespace " + namespace); + namespace.createVariable(varName, val); + } + return List.of(val); + + } + } + + private static class Constant implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 2) { + throw new Exception(); + } + String varName = args.get(0).toString(); + Value val = args.get(1); + namespace.createConstant(varName, val); + return List.of(val); + + } + } + + private static class Var implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 2) { + throw new Exception(); + } + String varName = args.get(0).toString(); + Value val = args.get(1); + namespace.createVariable(varName, val); + return List.of(val); + + } + } + + private static class Echo implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + return List.of(PString.of( + args.stream() + .map(Value::toString) + .collect(Collectors.joining()))); + } + + } + +} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ConnectionCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ConnectionCmds.java index 41b4adc5..6d896317 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ConnectionCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/ConnectionCmds.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -22,7 +22,7 @@ package org.praxislive.script.commands; import java.util.List; -import org.praxislive.script.impl.AbstractSingleCallFrame; +import org.praxislive.script.AbstractSingleCallFrame; import java.util.Map; import org.praxislive.core.Call; import org.praxislive.core.ComponentAddress; @@ -34,37 +34,31 @@ import org.praxislive.script.Command; import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; import org.praxislive.script.StackFrame; /** * */ -public class ConnectionCmds implements CommandInstaller { +class ConnectionCmds { - private final static ConnectionCmds instance = new ConnectionCmds(); private final static Connect CONNECT = new Connect(); private final static Disconnect DISCONNECT = new Disconnect(); private ConnectionCmds() { } - public void install(Map commands) { + static void install(Map commands) { commands.put("connect", CONNECT); commands.put("~", CONNECT); commands.put("disconnect", DISCONNECT); commands.put("!~", DISCONNECT); } - public final static ConnectionCmds getInstance() { - return instance; - } - private static class Connect implements Command { @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { return new ConnectionStackFrame(namespace, args, true); } } @@ -72,7 +66,7 @@ public StackFrame createStackFrame(Namespace namespace, List args) throws private static class Disconnect implements Command { @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { return new ConnectionStackFrame(namespace, args, false); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/CoreCommandsInstaller.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/CoreCommandsInstaller.java index 89e38d74..305c555b 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/CoreCommandsInstaller.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/CoreCommandsInstaller.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -32,14 +32,15 @@ */ public class CoreCommandsInstaller implements CommandInstaller { + @Override public void install(Map commands) { - ArrayCmds.getInstance().install(commands); - AtCmds.getInstance().install(commands); - ConnectionCmds.getInstance().install(commands); - FileCmds.getInstance().install(commands); - ResourceCmds.getInstance().install(commands); - ScriptCmds.getInstance().install(commands); - VariableCmds.getInstance().install(commands); + BaseCmds.install(commands); + ArrayCmds.install(commands); + AtCmds.install(commands); + ConnectionCmds.install(commands); + FileCmds.install(commands); + MapCmds.install(commands); + ScriptCmds.install(commands); } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java deleted file mode 100644 index e0f2a875..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2020 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ -package org.praxislive.script.commands; - -import java.util.LinkedList; -import java.util.List; -import java.util.logging.Logger; -import org.praxislive.core.Value; -import org.praxislive.core.Call; -import org.praxislive.core.ControlAddress; -import org.praxislive.core.types.PError; -import org.praxislive.script.Command; -import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; -import org.praxislive.script.Namespace; -import org.praxislive.script.StackFrame; -import org.praxislive.script.ast.RootNode; - -/** - * - */ -public class EvalStackFrame implements StackFrame { - - private final static Logger log = Logger.getLogger(EvalStackFrame.class.getName()); - private Namespace namespace; - private RootNode rootNode; - private State state; - private Call pending; - private List result; - private List argList; - private boolean doProcess; - - public EvalStackFrame(Namespace namespace, RootNode rootNode) { - this.namespace = namespace; - this.rootNode = rootNode; - this.state = State.Incomplete; - this.argList = new LinkedList<>(); - rootNode.reset(); - rootNode.init(namespace); - doProcess = true; - } - - @Override - public State getState() { - return state; - } - - @Override - public StackFrame process(Env context) { - if (state != State.Incomplete) { - throw new IllegalStateException(); - } - if (!doProcess) { - return null; - } - try { - if (rootNode.isDone()) { - processResultFromNode(); - return null; - } else { - return processNextCommand(context); - } - } catch (Exception ex) { - result = List.of(PError.of(ex)); - state = State.Error; - return null; - } finally { - doProcess = false; - } - - } - - @Override - public void postResponse(Call call) { - if (pending != null && pending.matchID() == call.matchID()) { - pending = null; - if (call.isReply()) { - log.finest("EvalStackFrame - Received valid Return call : \n" + call); - postResponse(call.args()); - } else { - log.finest("EvalStackFrame - Received valid Error call : \n" + call); - this.state = State.Error; - this.result = call.args(); - } - doProcess = true; - } else { - log.finest("EvalStackFrame - Received invalid call : \n" + call); - } - - } - - @Override - public void postResponse(State state, List args) { - if (this.state != State.Incomplete) { - throw new IllegalStateException(); - } - switch (state) { - case Incomplete: - throw new IllegalArgumentException(); - case OK: - postResponse(args); - break; - default: - this.state = state; - this.result = args; - } - doProcess = true; - } - - @Override - public List result() { - if (state == State.Incomplete) { - throw new IllegalStateException(); - } - if (result == null) { - return List.of(); - } else { - return result; - } - } - - private void postResponse(List args) { - try { - argList.clear(); - argList.addAll(args); - rootNode.postResponse(argList); - } catch (ExecutionException ex) { - state = State.Error;//@TODO proper error reporting - } - } - - private void processResultFromNode() throws ExecutionException { - argList.clear(); - rootNode.writeResult(argList); - result = List.copyOf(argList); - state = State.OK; - - } - - private StackFrame processNextCommand(Env context) - throws ExecutionException { - - argList.clear(); - rootNode.writeNextCommand(argList); - if (argList.size() < 1) { - throw new ExecutionException(); - } - Value cmdArg = argList.get(0); - if (cmdArg instanceof ControlAddress) { - routeCall(context, argList); - return null; - } - String cmdStr = cmdArg.toString(); - if (cmdStr.isEmpty()) { - throw new ExecutionException(); - } - Command cmd = namespace.getCommand(cmdStr); - if (cmd != null) { - argList.remove(0); - return cmd.createStackFrame(namespace, List.copyOf(argList)); - } - if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) { - routeCall(context, argList); - return null; - } - - throw new ExecutionException(); - - } - - private void routeCall(Env context, List argList) - throws ExecutionException { - ControlAddress ad = ControlAddress.from(argList.get(0)) - .orElseThrow(ExecutionException::new); - argList.remove(0); - Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList)); - log.finest("Sending Call" + call); - pending = call; - context.getPacketRouter().route(call); - } - -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/FileCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/FileCmds.java index 1ebb6eef..4e88a345 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/FileCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/FileCmds.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -21,7 +21,6 @@ */ package org.praxislive.script.commands; -import org.praxislive.script.impl.AbstractInlineCommand; import java.io.File; import java.io.IOException; import java.net.URI; @@ -36,50 +35,40 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; -import org.praxislive.core.ValueFormatException; import org.praxislive.core.Value; import org.praxislive.core.types.PArray; import org.praxislive.core.types.PResource; import org.praxislive.core.types.PString; import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; +import org.praxislive.script.InlineCommand; import org.praxislive.script.Namespace; -import org.praxislive.script.Variable; -import org.praxislive.script.impl.VariableImpl; +import org.praxislive.script.StackFrame; /** * */ -public class FileCmds implements CommandInstaller { - - private final static FileCmds INSTANCE = new FileCmds(); +class FileCmds { private final static Command FILE = new FileCmd(); private final static Command FILE_LIST = new FileListCmd(); private final static Command FILE_NAMES = new FileNamesCmd(); private final static Command CD = new CdCmd(); private final static Command PWD = new PwdCmd(); + private final static Command LOAD = new LoadCmd(); private FileCmds() { } - @Override - public void install(Map commands) { + static void install(Map commands) { commands.put("file", FILE); commands.put("file-list", FILE_LIST); commands.put("file-names", FILE_NAMES); commands.put("ls", FILE_NAMES); commands.put("cd", CD); commands.put("pwd", PWD); - } - - public static FileCmds getInstance() { - return INSTANCE; + commands.put("load", LOAD); } private static URI getPWD(Namespace namespace) { @@ -141,60 +130,57 @@ private static List listFiles(Namespace namespace, String path, String glo } } - private static class FileCmd extends AbstractInlineCommand { + private static class FileCmd implements InlineCommand { @Override - public List process(Env env, Namespace namespace, List args) throws ExecutionException { + public List process(Env env, Namespace namespace, List args) throws Exception { if (args.size() != 1) { - throw new ExecutionException(); + throw new Exception(); } try { return List.of(PResource.of( resolve(namespace, args.get(0).toString()) )); } catch (Exception ex) { - throw new ExecutionException(ex); + throw new Exception(ex); } } } - private static class FileListCmd extends AbstractInlineCommand { + private static class FileListCmd implements InlineCommand { @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { + public List process(Env context, Namespace namespace, List args) throws Exception { if (args.size() > 1) { - throw new ExecutionException(); + throw new Exception(); } try { List list; - /*if (args.getSize() == 2) { - list = listFiles(namespace, args.get(0).toString(), args.get(1).toString()); - } else*/ if (args.size() == 1) { list = listFiles(namespace, args.get(0).toString()); } else { list = listFiles(namespace); } - + List ret = list.stream() .map(path -> PResource.of(path.toUri())) .collect(Collectors.toList()); - + return List.of(PArray.of(ret)); } catch (Exception ex) { - throw new ExecutionException(ex); + throw new Exception(ex); } } } - private static class FileNamesCmd extends AbstractInlineCommand { + private static class FileNamesCmd implements InlineCommand { @Override - public List process(Env env, Namespace namespace, List args) throws ExecutionException { + public List process(Env env, Namespace namespace, List args) throws Exception { if (args.size() > 1) { - throw new ExecutionException(); + throw new Exception(); } try { List list; @@ -206,57 +192,67 @@ public List process(Env env, Namespace namespace, List args) throw } else { list = listFiles(namespace); } - + List ret = list.stream() .map(path -> PString.of(path.getFileName())) .collect(Collectors.toList()); - + return List.of(PArray.of(ret)); } catch (Exception ex) { - throw new ExecutionException(ex); + throw new Exception(ex); } } } - - private static class CdCmd extends AbstractInlineCommand { + + private static class CdCmd implements InlineCommand { @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { + public List process(Env context, Namespace namespace, List args) throws Exception { if (args.size() != 1) { - throw new ExecutionException(); + throw new Exception(); } try { URI uri = resolve(namespace, args.get(0).toString()); if ("file".equals(uri.getScheme())) { File d = new File(uri); if (!d.isDirectory()) { - throw new ExecutionException("Not a valid directory"); + throw new Exception("Not a valid directory"); } } PResource dir = PResource.of(uri); - Variable pwd = namespace.getVariable(Env.PWD); - if (pwd != null) { - pwd.setValue(dir); - } else { - pwd = new VariableImpl(dir); - namespace.addVariable(Env.PWD, pwd); - } + namespace.getOrCreateVariable(Env.PWD, dir).setValue(dir); return List.of(dir); } catch (URISyntaxException ex) { - throw new ExecutionException(ex); + throw new Exception(ex); } } - + } - - private static class PwdCmd extends AbstractInlineCommand { + + private static class PwdCmd implements InlineCommand { @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { + public List process(Env context, Namespace namespace, List args) throws Exception { return List.of(PResource.of(getPWD(namespace))); } - - } - + + } + + private static class LoadCmd implements Command { + + @Override + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { + if (args.size() != 1) { + throw new IllegalArgumentException("Wrong number of arguments"); + } + Path path = PResource.from(args.get(0)) + .map(PResource::value) + .map(Path::of) + .orElseThrow(IllegalArgumentException::new); + return StackFrame.async(() -> PString.of(Files.readString(path))); + } + + } + } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/MapCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/MapCmds.java new file mode 100644 index 00000000..83078165 --- /dev/null +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/MapCmds.java @@ -0,0 +1,136 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script.commands; + +import java.util.List; +import java.util.Map; +import org.praxislive.core.Value; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PMap; +import org.praxislive.core.types.PNumber; +import org.praxislive.core.types.PString; +import org.praxislive.script.Command; +import org.praxislive.script.Env; +import org.praxislive.script.InlineCommand; +import org.praxislive.script.Namespace; + +/** + * + */ +class MapCmds { + + private static final CreateMap MAP = new CreateMap(); + private static final MapGet MAP_GET = new MapGet(); + private static final MapKeys MAP_KEYS = new MapKeys(); + private static final MapSize MAP_SIZE = new MapSize(); + + private MapCmds() { + } + + static void install(Map commands) { + commands.put("map", MAP); + commands.put("map-get", MAP_GET); + commands.put("map-keys", MAP_KEYS); + commands.put("map-size", MAP_SIZE); + } + + private static class CreateMap implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.isEmpty()) { + return List.of(PMap.EMPTY); + } + + int size = args.size(); + if (size % 2 != 0) { + throw new IllegalArgumentException("Map requires an even number of arguments"); + } + + var builder = PMap.builder(); + for (int i = 0; i < size; i += 2) { + builder.put(args.get(i).toString(), args.get(i + 1)); + } + + return List.of(builder.build()); + } + + } + + private static class MapGet implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 2) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PMap map = PMap.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("Argument is not a map")); + String key = args.get(1).toString(); + + Value result = map.get(key); + if (result == null) { + throw new IllegalArgumentException("Unknown map key"); + } + return List.of(result); + } + + } + + private static class MapKeys implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 1) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PMap map = PMap.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("Argument is not a map")); + + PArray result = map.keys().stream() + .map(PString::of) + .collect(PArray.collector()); + + return List.of(result); + } + + } + + private static class MapSize implements InlineCommand { + + @Override + public List process(Env context, Namespace namespace, List args) throws Exception { + if (args.size() != 1) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + PMap map = PMap.from(args.get(0)) + .orElseThrow(() -> new IllegalArgumentException("Argument is not a map")); + + return List.of(PNumber.of(map.size())); + } + + } + +} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ResourceCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ResourceCmds.java deleted file mode 100644 index 4cdf2555..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ResourceCmds.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2020 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ -package org.praxislive.script.commands; - -import org.praxislive.script.impl.AbstractInlineCommand; -import java.io.File; -import java.util.List; -import java.util.Map; -import org.praxislive.core.Value; -import org.praxislive.core.types.PString; -import org.praxislive.core.types.PResource; -import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; -import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; -import org.praxislive.script.Namespace; - -/** - * - */ -public class ResourceCmds implements CommandInstaller { - - private final static ResourceCmds instance = new ResourceCmds(); - - private final static Command LOAD = new LoadCmd(); - - private ResourceCmds() { - } - - @Override - public void install(Map commands) { - commands.put("load", LOAD); - } - - public static ResourceCmds getInstance() { - return instance; - } - - - - private static class LoadCmd extends AbstractInlineCommand { - - @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { - if (args.size() != 1) { - throw new ExecutionException(); - } - try { - File f = new File(PResource.from(args.get(0)).orElseThrow().value()); - String s = Utils.loadStringFromFile(f); - return List.of(PString.of(s)); - } catch (Exception ex) { - throw new ExecutionException(ex); - } - } - - } -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java index 1e20157a..a534f5cb 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2020 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -21,96 +21,97 @@ */ package org.praxislive.script.commands; -import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; import java.util.List; import java.util.Map; +import java.util.Queue; import org.praxislive.core.Value; -import org.praxislive.core.syntax.InvalidSyntaxException; +import org.praxislive.core.types.PArray; import org.praxislive.core.types.PResource; +import org.praxislive.core.types.PString; import org.praxislive.script.Command; import org.praxislive.script.CommandInstaller; -import org.praxislive.script.ExecutionException; import org.praxislive.script.Namespace; +import org.praxislive.script.ScriptStackFrame; import org.praxislive.script.StackFrame; -import org.praxislive.script.ast.RootNode; -import org.praxislive.script.ast.ScriptParser; /** * */ -public class ScriptCmds implements CommandInstaller { +class ScriptCmds { - private final static ScriptCmds instance = new ScriptCmds(); public final static Command EVAL = new Eval(); - public final static Command INLINE_EVAL = new InlineEval(); public final static Command INCLUDE = new Include(); private ScriptCmds() { } - @Override - public void install(Map commands) { + static void install(Map commands) { commands.put("eval", EVAL); commands.put("include", INCLUDE); } - public static ScriptCmds getInstance() { - return instance; - } - private static class Eval implements Command { @Override public StackFrame createStackFrame(Namespace namespace, List args) - throws ExecutionException { - if (args.size() != 1) { - throw new ExecutionException(); + throws Exception { + if (args.isEmpty()) { + throw new IllegalArgumentException("No script passed to eval"); } - String script = args.get(0).toString(); - try { - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace.createChild(), astRoot); - } catch (InvalidSyntaxException ex) { - throw new ExecutionException(ex); + Queue queue = new ArrayDeque<>(args); + boolean inline = false; + boolean trap = false; + List allowed = null; + String script = null; + while (!queue.isEmpty()) { + String arg = queue.poll().toString(); + if ("--inline".equals(arg)) { + inline = true; + } else if ("--trap-errors".equals(arg)) { + trap = true; + if (allowed == null) { + allowed = List.of(); + } + } else if ("--allowed-commands".equals(arg)) { + allowed = PArray.from(queue.poll()).orElseThrow().asListOf(String.class); + } else { + script = arg; + break; + } } - } - } - - private static class InlineEval implements Command { - - @Override - public StackFrame createStackFrame(Namespace namespace, List args) - throws ExecutionException { - if (args.size() != 1) { - throw new ExecutionException(); + if (!queue.isEmpty()) { + throw new IllegalArgumentException("Additional arguments after script"); } - String script = args.get(0).toString(); - try { - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace, astRoot); - } catch (InvalidSyntaxException ex) { - throw new ExecutionException(ex); + var bld = ScriptStackFrame.forScript(namespace, script); + if (inline) { + bld.inline(); } + if (trap) { + bld.trapErrors(); + } + if (allowed != null) { + bld.allowedCommands(allowed); + } + return bld.build(); } } private static class Include implements Command { @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { - // @TODO - should load in background - call to + public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { if (args.size() != 1) { - throw new ExecutionException(); - } - try { - PResource res = PResource.from(args.get(0)).orElseThrow(); - File file = new File(res.value()); - String script = Utils.loadStringFromFile(file); - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace.createChild(), astRoot); - } catch (Exception ex) { - throw new ExecutionException(ex); + throw new IllegalArgumentException("Wrong number of arguments"); } + Path path = PResource.from(args.get(0)) + .map(PResource::value) + .map(Path::of) + .orElseThrow(IllegalArgumentException::new); + return StackFrame.async(() -> PString.of(Files.readString(path))) + .andThen(v -> ScriptStackFrame.forScript(namespace, v.get(0).toString()).build()); } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/Utils.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/Utils.java deleted file mode 100644 index 4a771bc9..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/Utils.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2018 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ -package org.praxislive.script.commands; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; - -/** - * - * - */ -class Utils { - - private Utils() { - } - - static String loadStringFromFile(File file) throws FileNotFoundException, IOException { - BufferedReader reader = new BufferedReader(new FileReader(file)); - StringBuilder data = new StringBuilder(); - char[] buf = new char[1024]; - int read = 0; - while ((read = reader.read(buf)) != -1) { - data.append(buf, 0, read); - } - reader.close(); - return data.toString(); - - } -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/VariableCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/VariableCmds.java deleted file mode 100644 index c5314687..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/VariableCmds.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2020 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ -package org.praxislive.script.commands; - -import java.util.List; -import org.praxislive.script.impl.AbstractInlineCommand; -import org.praxislive.script.impl.VariableImpl; -import java.util.Map; -import java.util.logging.Logger; -import org.praxislive.core.Value; -import org.praxislive.script.Command; -import org.praxislive.script.CommandInstaller; -import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; -import org.praxislive.script.Namespace; -import org.praxislive.script.Variable; - -/** - * - */ -public class VariableCmds implements CommandInstaller { - - private final static VariableCmds instance = new VariableCmds(); - - private final static Command SET = new Set(); - - - private final static Logger log = Logger.getLogger(VariableCmds.class.getName()); - - - private VariableCmds() {} - - @Override - public void install(Map commands) { - commands.put("set", SET); - } - - public static VariableCmds getInstance() { - return instance; - } - - private static class Set extends AbstractInlineCommand { - - @Override - public List process(Env context, Namespace namespace, List args) throws ExecutionException { - if (args.size() != 2) { - throw new ExecutionException(); - } - String varName = args.get(0).toString(); - Value val = args.get(1); - Variable var = namespace.getVariable(varName); - if (var != null) { - var.setValue(val); - } else { - log.finest("SET COMMAND : Adding variable " + varName + " to namespace " + namespace); - var = new VariableImpl(val); - namespace.addVariable(varName, var); - } - return List.of(val); - - } - } -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractInlineCommand.java b/praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractInlineCommand.java deleted file mode 100644 index 3bc69fda..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/impl/AbstractInlineCommand.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2018 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ - -package org.praxislive.script.impl; - -import java.util.List; -import org.praxislive.core.Call; -import org.praxislive.core.Value; -import org.praxislive.core.types.PReference; -import org.praxislive.script.Env; -import org.praxislive.script.ExecutionException; -import org.praxislive.script.InlineCommand; -import org.praxislive.script.Namespace; -import org.praxislive.script.StackFrame; - -/** - * - */ -public abstract class AbstractInlineCommand implements InlineCommand { - - - - @Override - public StackFrame createStackFrame(Namespace namespace, List args) throws ExecutionException { - return new InlineStackFrame(namespace, args); - } - - private class InlineStackFrame implements StackFrame { - - private final Namespace namespace; - private final List args; - private State state; - private List result; - - private InlineStackFrame(Namespace namespace, List args) { - this.namespace = namespace; - this.args = args; - state = State.Incomplete; - } - - @Override - public State getState() { - return state; - } - - @Override - public StackFrame process(Env env) { - if (state == State.Incomplete) { - try { - result = AbstractInlineCommand.this.process(env, namespace, args); - state = State.OK; - } catch (ExecutionException ex) { - result = List.of(PReference.of(ex)); - state = State.Error; - } - } - return null; - } - - @Override - public void postResponse(Call call) { - throw new IllegalStateException(); - } - - @Override - public void postResponse(State state, List args) { - throw new IllegalStateException(); - } - - @Override - public List result() { - if (result == null) { - throw new IllegalStateException(); - } - return result; - } - - } - -} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java new file mode 100644 index 00000000..3309aa6e --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java @@ -0,0 +1,330 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.praxislive.core.Call; +import org.praxislive.core.Clock; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Lookup; +import org.praxislive.core.Packet; +import org.praxislive.core.Root; +import org.praxislive.core.RootHub; +import org.praxislive.core.Value; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PError; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + */ +public class DefaultScriptServiceTest { + + private static final boolean VERBOSE = Boolean.getBoolean("praxis.test.verbose"); + private static final int TIMEOUT = Integer.getInteger("praxis.test.timeout", 10000); + + public DefaultScriptServiceTest() { + } + + @Test + public void testInlineScript() throws Exception { + logTest("testInlineScript"); + String script = """ + set V1 "One" + set V2 "Two" + set V3 $V1 + set RET [array $V1 $V2 $V3] + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + var result = hub.poll(); + logCall("Result received", result); + assertTrue(result.isReply()); + assertEquals(1, result.args().size()); + var expected = Stream.of("One", "Two", "One") + .map(Value::ofObject) + .collect(PArray.collector()); + assertEquals(expected, result.args().get(0)); + } + + } + + @Test + public void testAtScript() throws Exception { + logTest("testAtScript"); + String script = """ + .foo + @ /bar { + set V 42 + @ ./baz test:component { + .value $V + } + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Call to /hub.foo received", call); + assertTrue(call.isRequest()); + assertEquals("/hub.foo", call.to().toString()); + assertTrue(call.args().isEmpty()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar.add-child received", call); + assertTrue(call.isRequest()); + assertEquals("/bar.add-child", call.to().toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar/baz.value received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.value", call.to().toString()); + assertEquals(1, call.args().size()); + assertEquals("42", call.args().get(0).toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Result received", call); + + assertTrue(call.isReply()); + + } + + } + + @Test + public void testEvalInline() throws Exception { + logTest("testEvalInline"); + String script = """ + eval --inline { + var X 42 + /hub.value $X + } + set X [echo $X $X] + /hub.value $X + eval { + var Y 84 + /hub.value $Y + } + /hub.value $Y + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + + for (String expected : new String[]{"42", "4242", "84"}) { + Call call = hub.poll(); + logCall("Value received", call); + assertEquals("/hub.value", call.to().toString()); + assertEquals(expected, call.args().get(0).toString()); + hub.dispatch(call.reply()); + } + + Call call = hub.poll(); + logCall("Expected execution failure", call); + assertTrue(call.isError()); + + } + } + + @Test + public void testEvalTrapErrors() throws Exception { + logTest("testEvalTrapErrors"); + String script = """ + eval --trap-errors { + set X 42 + /hub.value "FOO" + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Value received", call); + assertEquals("/hub.value", call.to().toString()); + assertEquals("FOO", call.args().get(0).toString()); + hub.dispatch(call.reply()); + call = hub.poll(); + logCall("Error result received", call); + assertTrue(call.isError()); + } + + } + + @Test + public void testEvalTrapErrorsNested() throws Exception { + logTest("testEvalTrapErrorsNested"); + String script = """ + set allowed [array "@"] + eval --trap-errors --allowed-commands $allowed { + @ /bar { + set V 42 + @ ./baz test:component { + .error + .value "FOO" + } + } + set Y [set X] + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Call to /bar.add-child received", call); + assertTrue(call.isRequest()); + assertEquals("/bar.add-child", call.to().toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar/baz.error received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.error", call.to().toString()); + hub.dispatch(call.error(PError.of("BAZ ERROR"))); + + call = hub.poll(); + logCall("Call to /bar/baz.value received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.value", call.to().toString()); + assertEquals(1, call.args().size()); + assertEquals("FOO", call.args().get(0).toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Result received", call); + + assertTrue(call.isError()); + String errors = call.args().get(0).toString(); + if (VERBOSE) { + System.out.println(""); + System.out.println("Completed with errors"); + System.out.println("====================="); + System.out.println(errors); + } + assertTrue(errors.contains("set")); + assertTrue(errors.contains("BAZ ERROR")); + } + + } + + private static void logTest(String testName) { + if (VERBOSE) { + System.out.println(""); + System.out.println(""); + System.out.println(testName); + System.out.println("===================="); + } + } + + private static void logCall(String msg, Call call) { + if (VERBOSE) { + System.out.println(msg); + System.out.println(call); + } + } + + private static class RootHubImpl implements RootHub, AutoCloseable { + + private final String rootID; + private final Root root; + private final BlockingQueue queue; + + private Root.Controller ctrl; + + private RootHubImpl(String rootID, Root root) { + this.rootID = rootID; + this.root = root; + this.queue = new LinkedBlockingQueue<>(); + } + + @Override + public boolean dispatch(Packet packet) { + if (rootID.equals(packet.rootID())) { + return ctrl.submitPacket(packet); + } else { + return queue.offer(packet); + } + } + + @Override + public Clock getClock() { + return System::nanoTime; + } + + @Override + public Lookup getLookup() { + return Lookup.EMPTY; + } + + public void send(String to, String from, Object arg) { + send(to, from, List.of(Value.ofObject(arg))); + } + + public void send(String to, String from, List args) { + dispatch(Call.create(ControlAddress.of(to), + ControlAddress.of(from), + getClock().getTime(), + args + )); + } + + public Call poll() throws InterruptedException, TimeoutException { + Packet p = queue.poll(TIMEOUT, TimeUnit.MILLISECONDS); + if (p instanceof Call c) { + return c; + } + throw new TimeoutException("Call poll timed out"); + } + + public void start() { + ctrl = root.initialize(rootID, this); + ctrl.start(); + } + + @Override + public void close() { + ctrl.shutdown(); + try { + ctrl.awaitTermination(10, TimeUnit.SECONDS); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + } + +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/StackFrameTest.java b/praxiscore-script/src/test/java/org/praxislive/script/StackFrameTest.java new file mode 100644 index 00000000..27b341ad --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/StackFrameTest.java @@ -0,0 +1,196 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.praxislive.core.Call; +import org.praxislive.core.ComponentAddress; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Lookup; +import org.praxislive.core.Packet; +import org.praxislive.core.PacketRouter; +import org.praxislive.core.services.Service; +import org.praxislive.core.services.Services; +import org.praxislive.core.services.TaskService; +import org.praxislive.core.types.PNumber; +import org.praxislive.core.types.PReference; +import org.praxislive.core.types.PString; +import org.praxislive.script.StackFrame.State; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + */ +public class StackFrameTest { + + private static final boolean VERBOSE = Boolean.getBoolean("praxis.test.verbose"); +// private static final int TIMEOUT = Integer.getInteger("praxis.test.timeout", 10000); + + public StackFrameTest() { + } + + @Test + public void testCallStackFrame() { + logTest("testCallStackFrame"); + var env = new EnvImpl(); + var to = "/test.control"; + var frame = StackFrame.call(ControlAddress.of(to), PString.of("FOO")); + assertEquals(State.Incomplete, frame.getState()); + frame.process(env); + assertEquals(State.Incomplete, frame.getState()); + var call = env.poll(); + logCall("Received call", call); + assertEquals(to, call.to().toString()); + assertEquals(env.getTime(), call.time()); + assertEquals(EnvImpl.ADDRESS, call.from()); + assertEquals("FOO", call.args().get(0).toString()); + frame.postResponse(call.reply(PString.of("BAR"))); + assertEquals(State.OK, frame.getState()); + assertEquals("BAR", frame.result().get(0).toString()); + } + + @Test + public void testCallAndThenMapStackFrame() { + logTest("testCallAndThenMapStackFrame"); + var env = new EnvImpl(); + var to = "/test.control"; + var frame = StackFrame.call(ControlAddress.of(to), PString.of("FOO")) + .andThenMap(args -> { + if ("BAR".equals(args.get(0).toString())) { + return List.of(PString.of("BAZ")); + } else { + return List.of(PString.of("ERROR")); + } + }); + assertEquals(State.Incomplete, frame.getState()); + frame.process(env); + assertEquals(State.Incomplete, frame.getState()); + var call = env.poll(); + logCall("Received call", call); + assertEquals(to, call.to().toString()); + assertEquals(env.getTime(), call.time()); + assertEquals(EnvImpl.ADDRESS, call.from()); + assertEquals("FOO", call.args().get(0).toString()); + frame.postResponse(call.reply(PString.of("BAR"))); + assertEquals(State.Incomplete, frame.getState()); + frame.process(env); + assertEquals(State.OK, frame.getState()); + assertEquals("BAZ", frame.result().get(0).toString()); + } + + @Test + public void testAsyncStackFrame() throws Exception { + logTest("testAsyncStackFrame"); + var env = new EnvImpl(); + var frame = StackFrame.async(() -> PNumber.of(42)); + assertEquals(State.Incomplete, frame.getState()); + frame.process(env); + assertEquals(State.Incomplete, frame.getState()); + var call = env.poll(); + logCall("Received task", call); + assertEquals(EnvImpl.SERVICE, call.to().component()); + assertEquals(TaskService.SUBMIT, call.to().controlID()); + TaskService.Task task = PReference.from(call.args().get(0)) + .flatMap(r -> r.as(TaskService.Task.class)) + .orElseThrow(); + frame.postResponse(call.reply(task.execute())); + assertEquals(State.OK, frame.getState()); + assertEquals(42, PNumber.from(frame.result().get(0)).orElseThrow().toIntValue()); + } + + private static void logTest(String testName) { + if (VERBOSE) { + System.out.println(""); + System.out.println(""); + System.out.println(testName); + System.out.println("===================="); + } + } + + private static void logCall(String msg, Call call) { + if (VERBOSE) { + System.out.println(msg); + System.out.println(call); + } + } + + private static class EnvImpl implements Env { + + private static final ControlAddress ADDRESS = ControlAddress.of("/stack.eval"); + private static final ComponentAddress SERVICE = ComponentAddress.of("/service"); + + private final Queue queue; + private final Lookup lookup; + + private EnvImpl() { + this.queue = new LinkedList<>(); + this.lookup = Lookup.of(new Services() { + @Override + public Optional locate(Class service) { + return Optional.of(SERVICE); + } + + @Override + public Stream locateAll(Class service) { + return locate(service).stream(); + } + }); + } + + @Override + public ControlAddress getAddress() { + return ADDRESS; + } + + @Override + public Lookup getLookup() { + return lookup; + } + + @Override + public PacketRouter getPacketRouter() { + return queue::add; + } + + @Override + public long getTime() { + return 12345; + } + + public Call poll() { + Packet p = queue.poll(); + if (p instanceof Call c) { + return c; + } else { + return null; + } + } + + } + +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java index c96eff8f..2b7f9c4b 100644 --- a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java +++ b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java @@ -1,95 +1,51 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ - package org.praxislive.script.ast; -import org.praxislive.script.ast.AddressNode; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Test; import org.praxislive.core.Value; import org.praxislive.core.ComponentAddress; import org.praxislive.script.Command; import org.praxislive.script.Namespace; import org.praxislive.script.Variable; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import static org.junit.Assert.*; - -/** - * - * - */ -public class AddressNodeTest { - public AddressNodeTest() { - } +import static org.junit.jupiter.api.Assertions.*; - @BeforeClass - public static void setUpClass() throws Exception { - } - @AfterClass - public static void tearDownClass() throws Exception { - } - - @Before - public void setUp() { - } - - @After - public void tearDown() { - } - -// /** -// * Test of init method, of class AddressNode. -// */ -// @Test -// public void testInit() { -// System.out.println("init"); -// Namespace namespace = null; -// AddressNode instance = null; -// instance.init(namespace); -// // TODO review the generated test code and remove the default call to fail. -// fail("The test case is a prototype."); -// } -// -// /** -// * Test of reset method, of class AddressNode. -// */ -// @Test -// public void testReset() { -// System.out.println("reset"); -// AddressNode instance = null; -// instance.reset(); -// // TODO review the generated test code and remove the default call to fail. -// fail("The test case is a prototype."); -// } +public class AddressNodeTest { /** * Test of writeResult method, of class AddressNode. */ @Test public void testWriteResult() { - System.out.println("writeResult"); - List args = new ArrayList(); Namespace ns = new NS(); - String[] ads = {"./to/here", "./to/here.control", "./to/here!port", ".control2"}; - for (String ad : ads) { - AddressNode instance = new AddressNode(ad); - instance.init(ns); - instance.writeResult(args); - System.out.println(ad + " : " + args.get(0)); - args.clear(); - } + List scratch = new ArrayList<>(); + List relative = List.of( + "./to/here", + "./to/here.control", + "./to/here!port", + ".control2"); + List absolute = List.of( + "/test/address/to/here", + "/test/address/to/here.control", + "/test/address/to/here!port", + "/test/address.control2"); + List result = relative.stream() + .map(address -> { + var addressNode = new AddressNode(address); + addressNode.init(ns); + scratch.clear(); + addressNode.writeResult(scratch); + return scratch.get(0).toString(); + }).toList(); + assertEquals(absolute, result); + } private class NS implements Namespace, Variable { + @Override public Variable getVariable(String id) { if ("_CTXT".equals(id)) { return this; @@ -97,30 +53,36 @@ public Variable getVariable(String id) { return null; } + @Override public void addVariable(String id, Variable var) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public Namespace createChild() { throw new UnsupportedOperationException("Not supported yet."); } + @Override public void setValue(Value value) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public Value getValue() { return ComponentAddress.of("/test/address"); } + @Override public Command getCommand(String id) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public void addCommand(String id, Command cmd) { throw new UnsupportedOperationException("Not supported yet."); } } -} \ No newline at end of file +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/commands/ArrayCmdsTest.java b/praxiscore-script/src/test/java/org/praxislive/script/commands/ArrayCmdsTest.java new file mode 100644 index 00000000..9ef5749e --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/commands/ArrayCmdsTest.java @@ -0,0 +1,206 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script.commands; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Value; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PBoolean; +import org.praxislive.core.types.PNumber; +import org.praxislive.core.types.PString; +import org.praxislive.script.Command; +import org.praxislive.script.InlineCommand; + +import static org.junit.jupiter.api.Assertions.*; +import static org.praxislive.script.commands.Utils.*; + +/** + * + */ +public class ArrayCmdsTest { + + private static final int V0 = 42; + private static final boolean V1 = true; + private static final String V2 = "FOO"; + private static final PArray V3 = PArray.of( + ControlAddress.of("/root.info"), + ControlAddress.of("/root.meta") + ); + + private static final Map CMDS; + + static { + CMDS = new HashMap<>(); + ArrayCmds.install(CMDS); + } + + @Test + public void testArrayCommand() throws Exception { + logTest("testArrayCommand"); + InlineCommand array = (InlineCommand) CMDS.get("array"); + List values = List.of( + Value.ofObject(V0), + Value.ofObject(V1), + Value.ofObject(V2), + Value.ofObject(V3) + ); + List resultList = array.process(env(), namespace(), values); + logResult("Command result of array", resultList); + assertEquals(1, resultList.size()); + PArray result = PArray.from(resultList.get(0)).orElseThrow(); + assertEquals(4, result.size()); + assertEquals(V0, PNumber.from(result.get(0)).orElseThrow().toIntValue()); + assertEquals(V1, PBoolean.from(result.get(1)).orElseThrow().value()); + assertEquals(V2, PString.from(result.get(2)).orElseThrow().value()); + assertEquals(V3, result.get(3)); + + resultList = array.process(env(), namespace(), List.of()); + logResult("Command result of array no-args", resultList); + assertEquals(1, resultList.size()); + result = PArray.from(resultList.get(0)).orElseThrow(); + assertSame(PArray.EMPTY, result); + } + + @Test + public void testArrayGetCommand() throws Exception { + logTest("testArrayGetCommand"); + InlineCommand arrayGet = (InlineCommand) CMDS.get("array-get"); + PArray array = Stream.of(V0, V1, V2, V3) + .map(Value::ofObject) + .collect(PArray.collector()); + + List resultList = arrayGet.process(env(), namespace(), + List.of(array, PNumber.of(2))); + logResult("Command result of array-get 2", resultList); + assertEquals(1, resultList.size()); + Value result = resultList.get(0); + assertEquals(V2, result.toString()); + resultList = arrayGet.process(env(), namespace(), + List.of(array, PNumber.of(5))); + logResult("Command result of array-get 5", resultList); + assertEquals(1, resultList.size()); + result = resultList.get(0); + assertEquals(V1, PBoolean.from(result).orElseThrow().value()); + resultList = arrayGet.process(env(), namespace(), + List.of(array, PNumber.of(-1))); + logResult("Command result of array-get -1", resultList); + assertEquals(1, resultList.size()); + result = resultList.get(0); + assertEquals(V3, result); + + resultList = arrayGet.process(env(), namespace(), + List.of(PArray.EMPTY, PNumber.of(-1))); + logResult("Command result of array-get -1 on empty array", resultList); + assertEquals(1, resultList.size()); + result = resultList.get(0); + assertSame(PArray.EMPTY, result); + + assertThrows(IllegalArgumentException.class, () -> { + List noResult = arrayGet.process(env(), namespace(), List.of(PNumber.ONE)); + }); + + } + + @Test + public void testArrayJoinCommand() throws Exception { + logTest("testArrayJoinCommand"); + InlineCommand arrayJoin = (InlineCommand) CMDS.get("array-join"); + PArray array1 = Stream.of(V0, V1, V2, V3) + .map(Value::ofObject) + .collect(PArray.collector()); + PArray array2 = Stream.of(V3, V2, V1, V0) + .map(Value::ofObject) + .collect(PArray.collector()); + List resultList = arrayJoin.process(env(), namespace(), + List.of(array1, array2)); + logResult("Command result of array-join", resultList); + assertEquals(1, resultList.size()); + PArray result = PArray.from(resultList.get(0)).orElseThrow(); + assertEquals(8, result.size()); + PArray expected = Stream.of(V0, V1, V2, V3, V3, V2, V1, V0) + .map(Value::ofObject) + .collect(PArray.collector()); + assertEquals(expected, result); + + resultList = arrayJoin.process(env(), namespace(), List.of(array1, PArray.EMPTY)); + logResult("Command result of array-join empty with empty", resultList); + result = PArray.from(resultList.get(0)).orElseThrow(); + assertEquals(array1, result); + + } + + @Test + public void testArrayRangeCommand() throws Exception { + logTest("testArrayRangeCommand"); + InlineCommand arrayRange = (InlineCommand) CMDS.get("array-range"); + PArray array = Stream.of(V0, V1, V2, V3) + .map(Value::ofObject) + .collect(PArray.collector()); + List resultList = arrayRange.process(env(), namespace(), + List.of(array, PNumber.of(3))); + logResult("Command result of array-range 3", resultList); + assertEquals(1, resultList.size()); + PArray result = PArray.from(resultList.get(0)).orElseThrow(); + assertEquals(3, result.size()); + PArray expected = Stream.of(V0, V1, V2) + .map(Value::ofObject) + .collect(PArray.collector()); + assertEquals(expected, result); + resultList = arrayRange.process(env(), namespace(), + List.of(array, PNumber.of(1), PNumber.of(3))); + logResult("Command result of array-range 1 3", resultList); + assertEquals(1, resultList.size()); + result = PArray.from(resultList.get(0)).orElseThrow(); + assertEquals(2, result.size()); + expected = Stream.of(V1, V2) + .map(Value::ofObject) + .collect(PArray.collector()); + assertEquals(expected, result); + + assertThrows(IndexOutOfBoundsException.class, () -> { + List failResult = arrayRange.process(env(), namespace(), + List.of(array, PNumber.of(1), PNumber.of(5))); + }); + + } + + @Test + public void testArraySizeCommand() throws Exception { + logTest("testArraySizeCommand"); + InlineCommand arraySize = (InlineCommand) CMDS.get("array-size"); + PArray array = Stream.of(V0, V1, V2, V3) + .map(Value::ofObject) + .collect(PArray.collector()); + List resultList = arraySize.process(env(), namespace(), + List.of(array)); + logResult("Command result of array-size", resultList); + assertEquals(1, resultList.size()); + int result = PNumber.from(resultList.get(0)).orElseThrow().toIntValue(); + assertEquals(4, result); + } + +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/commands/MapCmdsTest.java b/praxiscore-script/src/test/java/org/praxislive/script/commands/MapCmdsTest.java new file mode 100644 index 00000000..7dce7264 --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/commands/MapCmdsTest.java @@ -0,0 +1,150 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script.commands; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Value; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PBoolean; +import org.praxislive.core.types.PMap; +import org.praxislive.core.types.PNumber; +import org.praxislive.core.types.PString; +import org.praxislive.script.Command; +import org.praxislive.script.InlineCommand; + +import static org.junit.jupiter.api.Assertions.*; +import static org.praxislive.script.commands.Utils.*; + +/** + * + */ +public class MapCmdsTest { + + private static final String K1 = "Key1"; + private static final String K2 = "Key2"; + private static final String K3 = "Key3"; + private static final String K4 = "Key4"; + + private static final int V1 = 42; + private static final boolean V2 = true; + private static final String V3 = "FOO"; + private static final PArray V4 = PArray.of( + ControlAddress.of("/root.info"), + ControlAddress.of("/root.meta") + ); + + private static final PMap BASE_MAP = PMap.of( + K1, V1, K2, V2, K3, V3, K4, V4 + ); + + private static final Map CMDS; + + static { + CMDS = new HashMap<>(); + MapCmds.install(CMDS); + } + + @Test + public void testMapCommand() throws Exception { + logTest("testMapCommand"); + InlineCommand map = (InlineCommand) CMDS.get("map"); + List resultList = map.process(env(), namespace(), + List.of( + PString.of(K1), Value.ofObject(V1), + PString.of(K2), Value.ofObject(V2), + PString.of(K3), Value.ofObject(V3), + PString.of(K4), Value.ofObject(V4) + )); + logResult("Command result of map", resultList); + assertEquals(1, resultList.size()); + PMap result = PMap.from(resultList.get(0)).orElseThrow(); + assertEquals(BASE_MAP, result); + + resultList = map.process(env(), namespace(), List.of()); + logResult("Command result of map-empty", resultList); + assertEquals(1, resultList.size()); + result = PMap.from(resultList.get(0)).orElseThrow(); + assertSame(PMap.EMPTY, result); + + assertThrows(IllegalArgumentException.class, () -> { + List failResult = map.process(env(), namespace(), + List.of( + PString.of(K1), Value.ofObject(V1), + PString.of(K2), Value.ofObject(V2), + PString.of(K3), Value.ofObject(V3), + PString.of(K4) + )); + }); + + } + + @Test + public void testMapGetCommand() throws Exception { + logTest("testMapGetCommand"); + InlineCommand mapGet = (InlineCommand) CMDS.get("map-get"); + List resultList = mapGet.process(env(), namespace(), + List.of(BASE_MAP, PString.of(K3))); + logResult("Command result of map-get key3", resultList); + assertEquals(1, resultList.size()); + assertEquals(V3, resultList.get(0).toString()); + + assertThrows(IllegalArgumentException.class, () -> { + List failResult = mapGet.process(env(), namespace(), + List.of(PMap.EMPTY, PString.of(K3))); + }); + + } + + @Test + public void testMapKeysCommand() throws Exception { + logTest("testMapKeysCommand"); + InlineCommand mapKeys = (InlineCommand) CMDS.get("map-keys"); + List resultList = mapKeys.process(env(), namespace(), List.of(BASE_MAP)); + logResult("Command result of map-keys", resultList); + assertEquals(1, resultList.size()); + PArray result = PArray.from(resultList.get(0)).orElseThrow(); + PArray expected = PArray.of( + PString.of(K1), + PString.of(K2), + PString.of(K3), + PString.of(K4) + ); + assertEquals(expected, result); + } + + @Test + public void testMapSizeCommand() throws Exception { + logTest("testMapSizeCommand"); + InlineCommand mapSize = (InlineCommand) CMDS.get("map-size"); + List resultList = mapSize.process(env(), namespace(), List.of(BASE_MAP)); + logResult("Command result of map-size", resultList); + assertEquals(1, resultList.size()); + int result = PNumber.from(resultList.get(0)).orElseThrow().toIntValue(); + assertEquals(BASE_MAP.size(), result); + } + +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/commands/Utils.java b/praxiscore-script/src/test/java/org/praxislive/script/commands/Utils.java new file mode 100644 index 00000000..079801ca --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/commands/Utils.java @@ -0,0 +1,143 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script.commands; + +import java.util.HashMap; +import java.util.Map; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Lookup; +import org.praxislive.core.PacketRouter; +import org.praxislive.script.Command; +import org.praxislive.script.Env; +import org.praxislive.script.Namespace; +import org.praxislive.script.Variable; + +/** + * + */ +class Utils { + + private static final boolean VERBOSE = Boolean.getBoolean("praxis.test.verbose"); + + private Utils() { + } + + static Env env() { + return new EmptyEnv(); + } + + static Namespace namespace() { + return new NS(); + } + + static void logTest(String testName) { + if (VERBOSE) { + System.out.println(); + System.out.println(testName); + System.out.println("=================="); + } + } + + static void logResult(String description, Object value) { + if (VERBOSE) { + System.out.println(description); + System.out.println(value); + } + } + + private static class EmptyEnv implements Env { + + @Override + public ControlAddress getAddress() { + return ControlAddress.of("/dev.null"); + } + + @Override + public Lookup getLookup() { + return Lookup.EMPTY; + } + + @Override + public PacketRouter getPacketRouter() { + return p -> { + }; + } + + @Override + public long getTime() { + return System.nanoTime(); + } + + } + + private static class NS implements Namespace { + + private final NS parent; + private final Map variables; + private final Map commands; + + private NS() { + this(null); + } + + private NS(NS parent) { + this.parent = parent; + variables = new HashMap<>(); + commands = Map.of(); + } + + @Override + public Variable getVariable(String id) { + Variable var = variables.get(id); + if (var == null && parent != null) { + return parent.getVariable(id); + } else { + return var; + } + } + + @Override + public void addVariable(String id, Variable var) { + if (variables.containsKey(id)) { + throw new IllegalArgumentException(); + } + variables.put(id, var); + } + + @Override + public Command getCommand(String id) { + return commands.get(id); + } + + @Override + public void addCommand(String id, Command cmd) { + throw new UnsupportedOperationException(); + } + + @Override + public Namespace createChild() { + return new NS(this); + } + + } + +}