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 extends Service> 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 extends Service> 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 extends Node> 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 extends Service> service) {
+ return Optional.of(SERVICE);
+ }
+
+ @Override
+ public Stream locateAll(Class extends Service> 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);
+ }
+
+ }
+
+}