Browse Source

Added some utils, more tests, more bug fixes

crazy_things
Ludovic 'Archivist' Lagouardette 1 year ago
parent
commit
8f97e3f52f
10 changed files with 375 additions and 105 deletions
  1. +2
    -1
      CMakeLists.txt
  2. +14
    -2
      include/UserScript.h
  3. +28
    -0
      priv_include/UserScript/interpreter.h
  4. +63
    -89
      script_exe/main.cpp
  5. +6
    -6
      src/std/array.cpp
  6. +5
    -5
      src/std/crypto.cpp
  7. +227
    -0
      src/std/utils.cpp
  8. +4
    -2
      tests/parser_test.cpp
  9. +5
    -0
      tests/scripts/008.results
  10. +21
    -0
      tests/scripts/008.script

+ 2
- 1
CMakeLists.txt View File

@ -25,7 +25,7 @@ add_library(UserScript STATIC
src/generator.cpp
src/interpreter.cpp
src/lex.cpp
src/parse.cpp src/std/array.cpp src/std/crypto.cpp include/UserScriptRequire.h include/UserScriptWizardry.h)
src/parse.cpp src/std/array.cpp src/std/crypto.cpp include/UserScriptRequire.h include/UserScriptWizardry.h src/std/utils.cpp)
target_include_directories(UserScript PUBLIC include)
include_directories(priv_include)
@ -53,3 +53,4 @@ add_script_test("Scripting 004: While loops with bad terminator" tests/scrip
add_script_test("Scripting 005: If statements with bad terminator" tests/scripts/005.script tests/scripts/005.results)
add_script_test("Scripting 006: The stack is properly purged" tests/scripts/006.script tests/scripts/006.results)
add_script_test("Scripting 007: Cryptography seems to work" tests/scripts/007.script tests/scripts/007.results)
add_script_test("Scripting 008: Utils seems to work" tests/scripts/008.script tests/scripts/008.results)

+ 14
- 2
include/UserScript.h View File

@ -51,6 +51,10 @@ namespace scripting {
virtual void registerFunction(std::string name, function fn) = 0;
virtual void clear_variables() = 0;
virtual int32_t op_count() = 0;
virtual int32_t var_count() = 0;
virtual void var_clear() = 0;
virtual std::optional<script_value> varname_by_idx(int32_t idx) = 0;
virtual std::optional<script_value> var_by_idx(int32_t idx) = 0;
virtual script_value resolve(const std::string& name) = 0;
virtual std::variant<script_value, std::vector<script_error>> executeAtOnce(std::string code) = 0;
virtual std::vector<script_error> prepare(std::string code) = 0;
@ -58,7 +62,15 @@ namespace scripting {
virtual ~UserScript() = default;
};
struct UserScriptLibraryParameters {
bool recursive_arrays = false;
int32_t array_size_limit = 1024;
int32_t string_size_limit = 1024;
int32_t variables_count = 1024;
};
std::unique_ptr<UserScript> prepare_interpreter(const std::string& code);
std::unique_ptr<UserScript> register_array_lib(std::unique_ptr<UserScript> target, bool recursive_arrays = false, int32_t size_limit = 1024);
std::unique_ptr<UserScript> register_crypto_lib(std::unique_ptr<UserScript> target, int32_t array_size_limit = 1024, int32_t string_size_limit = 1024);
std::unique_ptr<UserScript> register_array_lib(std::unique_ptr<UserScript> target, const UserScriptLibraryParameters&);
std::unique_ptr<UserScript> register_crypto_lib(std::unique_ptr<UserScript> target, const UserScriptLibraryParameters&);
std::unique_ptr<UserScript> register_utils_lib(std::unique_ptr<UserScript> target, const UserScriptLibraryParameters&);
}

+ 28
- 0
priv_include/UserScript/interpreter.h View File

@ -121,6 +121,34 @@ namespace scripting {
return bytecode.size();
}
int32_t var_count() final {
return variables.size();
}
void var_clear() final {
variables.clear();
}
std::optional<script_value> var_by_idx(int32_t idx) final {
if(idx >= var_count() || idx < 0) return std::nullopt;
for(auto& val : variables) {
if(not idx--) {
return val.second;
}
}
return std::nullopt;
}
std::optional<script_value> varname_by_idx(int32_t idx) final {
if(idx >= var_count() || idx < 0) return std::nullopt;
for(auto& val : variables) {
if(not idx--) {
return val.first;
}
}
return std::nullopt;
}
std::variant<script_value, std::vector<script_error>> executeAtOnce(std::string code) final {
std::vector<script_error> errors;
auto lexed = ast::lex(code, errors);

+ 63
- 89
script_exe/main.cpp View File

@ -32,21 +32,6 @@ void print_value(std::ostream& stream, const scripting::script_value& res, bool
}
}
struct identity : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
if(args.size() != 1) {
errors = scripting::script_error{.message = "identity expects a single argument"};
} else {
if(std::holds_alternative<scripting::script_value>(args.front())) {
return std::get<scripting::script_value>(args.front());
} else {
return self->resolve(std::get<scripting::script_variable>(args.front()).name);
}
}
return scripting::script_value({});
}
};
struct print : public scripting::function_impl {
std::ostream& stream;
@ -67,39 +52,6 @@ struct print : public scripting::function_impl {
}
};
struct set : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
if(args.size() != 2) {
errors = scripting::script_error{
.message = "set expects 2 arguments"
};
return scripting::script_value{};
}
auto& var = args.back();
if(not holds_alternative<scripting::script_variable>(var)) {
errors = scripting::script_error{
.message = "set expects the first argument to be a target variable"
};
return scripting::script_value{};
}
auto& arg = args.front();
if(std::holds_alternative<scripting::script_value>(arg)) {
self->setValue(get<scripting::script_variable>(var).name, std::get<scripting::script_value>(arg));
} else {
self->setValue(get<scripting::script_variable>(var).name, self->resolve(std::get<scripting::script_variable>(arg).name));
}
if(auto v = self->getValue(get<scripting::script_variable>(var).name); v) {
return v.value();
} else {
return scripting::script_value{};
}
}
};
struct terminate : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript*,std::vector<scripting::argument>, std::optional<scripting::script_error>&) final {
std::exit(1);
@ -122,9 +74,14 @@ struct fn_exit : public scripting::function_impl {
void process_bench(std::string target = "./tests/scripts/testfile.test") {
auto engine = scripting::prepare_interpreter(std::string{});
engine->registerFunction("identity", std::make_unique<identity>());
constexpr scripting::UserScriptLibraryParameters params{};
engine = scripting::register_array_lib(std::move(engine), params);
engine = scripting::register_crypto_lib(std::move(engine), params);
engine = scripting::register_utils_lib(std::move(engine), params);
engine->registerFunction("exit", std::make_unique<terminate>());
engine->registerFunction("set", std::make_unique<set>());
/***
* This is a half assed benchmark,
@ -178,9 +135,14 @@ void process_bench(std::string target = "./tests/scripts/testfile.test") {
void compile_bench(std::string target = "./tests/scripts/testfile.test") {
auto engine = scripting::prepare_interpreter(std::string{});
engine->registerFunction("identity", std::make_unique<identity>());
constexpr scripting::UserScriptLibraryParameters params{};
engine = scripting::register_array_lib(std::move(engine), params);
engine = scripting::register_crypto_lib(std::move(engine), params);
engine = scripting::register_utils_lib(std::move(engine), params);
engine->registerFunction("exit", std::make_unique<terminate>());
engine->registerFunction("set", std::make_unique<set>());
/***
* Same as above but for compilation times
@ -220,15 +182,13 @@ void compile_bench(std::string target = "./tests/scripts/testfile.test") {
void compare(std::string target, std::string expect) {
auto engine = scripting::prepare_interpreter(std::string{});
engine->registerFunction("identity", std::make_unique<identity>());
engine->registerFunction("exit", std::make_unique<terminate>());
engine->registerFunction("set", std::make_unique<set>());
constexpr size_t array_limit = 4096;
constexpr size_t string_limit = 4096;
constexpr scripting::UserScriptLibraryParameters params{};
engine = scripting::register_array_lib(std::move(engine), true, array_limit);
engine = scripting::register_crypto_lib(std::move(engine), array_limit, string_limit);
engine = scripting::register_array_lib(std::move(engine), params);
engine = scripting::register_crypto_lib(std::move(engine), params);
engine = scripting::register_utils_lib(std::move(engine), params);
std::stringstream str;
std::string_view filename_source = target;
@ -274,15 +234,13 @@ void immediate_interactive() {
bool should_exit = false;
auto engine = scripting::prepare_interpreter(std::string{});
engine->registerFunction("identity", std::make_unique<identity>());
engine->registerFunction("exit", std::make_unique<fn_exit>(should_exit));
engine->registerFunction("set", std::make_unique<set>());
constexpr scripting::UserScriptLibraryParameters params{};
constexpr size_t array_limit = 4096;
constexpr size_t string_limit = 4096;
engine = scripting::register_array_lib(std::move(engine), params);
engine = scripting::register_crypto_lib(std::move(engine), params);
engine = scripting::register_utils_lib(std::move(engine), params);
engine = scripting::register_array_lib(std::move(engine), true, array_limit);
engine = scripting::register_crypto_lib(std::move(engine), array_limit, string_limit);
engine->registerFunction("exit", std::make_unique<fn_exit>(should_exit));
engine->registerFunction("print", std::make_unique<print>(std::cout));
while (not should_exit) {
@ -306,38 +264,54 @@ void immediate_interactive() {
}
void exec(std::span<std::string_view> args) {
std::vector<decltype(scripting::prepare_interpreter(std::string{}))> batch;
//std::vector<decltype(scripting::prepare_interpreter(std::string{}))> batch;
std::ifstream src_str(std::string{args.front()});
std::stringstream code;
code << src_str.rdbuf();
std::string code_val = code.str();
auto engine = scripting::prepare_interpreter(std::string{});
bool exit = false;
engine->registerFunction("identity", std::make_unique<identity>());
engine->registerFunction("terminate", std::make_unique<terminate>());
engine->registerFunction("set", std::make_unique<set>());
constexpr scripting::UserScriptLibraryParameters params{};
constexpr size_t array_limit = 4096;
constexpr size_t string_limit = 4096;
engine = scripting::register_array_lib(std::move(engine), params);
engine = scripting::register_crypto_lib(std::move(engine), params);
engine = scripting::register_utils_lib(std::move(engine), params);
engine = scripting::register_array_lib(std::move(engine), true, array_limit);
engine = scripting::register_crypto_lib(std::move(engine), array_limit, string_limit);
engine->registerFunction("exit", std::make_unique<fn_exit>(exit));
engine->registerFunction("print", std::make_unique<print>(std::cout));
bool exit = false;
auto errors = engine->prepare(code_val);
if(not errors.empty()) {
for (auto &line: errors) {
std::cout << line.message << "\n at line ";
if(line.location) {
std::cout << line.location->line_number << ":"
<< line.location->column_number << "\n";
std::cout << " " << *line.location->line_contents << "\n";
std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n";
} else std::cout << "UNKNOWN\n";
}
return;
}
while (not exit) {
std::string code;
std::getline(std::cin, code);
auto res = engine->executeAtOnce(code);
if (std::holds_alternative<scripting::script_value>(res)) {
auto res = engine->stepOnce();
if (not res) {
} else {
auto &errors = std::get<std::vector<scripting::script_error>>(res);
for (auto &line: errors) {
std::cout << line.message << "\n at line ";
if(line.location) {
std::cout << line.location->line_number << ":"
<< line.location->column_number << "\n";
std::cout << " " << *line.location->line_contents << "\n";
std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n";
} else std::cout << "UNKNOWN\n";
}
auto line = res.value();
std::cout << line.message << "\n at line ";
if(line.location) {
std::cout << line.location->line_number << ":"
<< line.location->column_number << "\n";
std::cout << " " << *line.location->line_contents << "\n";
std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n";
} else std::cout << "UNKNOWN\n";
}
}
}
@ -380,7 +354,7 @@ int cpp_main(std::span args) {
if(args.empty()) compile_bench();
else compile_bench(std::string{args.front()});
} else if(args.front() == "exec") {
c1">// exec(args.subspan(1));
n">exec(args.subspan(1));
} else {
std::cerr << "Unknown option" << std::endl;
}

+ 6
- 6
src/std/array.cpp View File

@ -18,7 +18,7 @@ struct fn_array final : public scripting::function_impl {
std::optional<script_value> apply(UserScript* self, std::vector<argument> n, std::optional<script_error>& error) final {
array ary;
if(not can_contain_arrays && details::NoArrays<0>{}.verify(self, n)) {
if(not can_contain_arrays && not details::NoArrays<0>{}.verify(self, n)) {
error = script_error{
.message = "/array: arrays cannot contain other arrays"
};
@ -402,16 +402,16 @@ struct fn_array_set final : public scripting::function_impl {
};
namespace scripting {
interpreter register_array_lib(interpreter target, t">bool recursive_arrays, int32_t size_limit) {
target->registerFunction("array", std::make_unique<fn_array>(size_limit, recursive_arrays));
interpreter register_array_lib(interpreter target, ">const UserScriptLibraryParameters& params) {
target->registerFunction("array", std::make_unique<fn_array>(params.array_size_limit, params.recursive_arrays));
target->registerFunction("array_reverse", std::make_unique<fn_array_reverse>());
target->registerFunction("array_append", std::make_unique<fn_array_append>("array_append", size_limit, recursive_arrays));
target->registerFunction("array_push", std::make_unique<fn_array_append>("array_push", size_limit, recursive_arrays));
target->registerFunction("array_append", std::make_unique<fn_array_append>("array_append", params.array_size_limit, params.recursive_arrays));
target->registerFunction("array_push", std::make_unique<fn_array_append>("array_push", params.array_size_limit, params.recursive_arrays));
target->registerFunction("array_pop", std::make_unique<fn_array_pop>("array_pop"));
target->registerFunction("array_size", std::make_unique<fn_array_size>());
target->registerFunction("array_index", std::make_unique<fn_array_index>());
target->registerFunction("array_set", std::make_unique<fn_array_set>());
target->registerFunction("queue_enqueue", std::make_unique<fn_array_prepend>("queue_enqueue", size_limit, recursive_arrays));
target->registerFunction("queue_enqueue", std::make_unique<fn_array_prepend>("queue_enqueue", params.array_size_limit, params.recursive_arrays));
target->registerFunction("queue_dequeue", std::make_unique<fn_array_pop>("queue_dequeue"));
return std::move(target);
}

+ 5
- 5
src/std/crypto.cpp View File

@ -375,11 +375,11 @@ namespace scripting {
~fn_decode_n() final = default;
};
interpreter register_crypto_lib(interpreter target, t">int32_t array_size_limit, int32_t string_size_limit) {
target->registerFunction("string_to_binary", std::make_unique<fn_string_to_binary>(array_size_limit));
target->registerFunction("binary_to_string", std::make_unique<fn_binary_to_string>(string_size_limit));
target->registerFunction("encode_n", std::make_unique<fn_encode_n>(string_size_limit));
target->registerFunction("decode_n", std::make_unique<fn_decode_n>(string_size_limit));
interpreter register_crypto_lib(interpreter target, ">const UserScriptLibraryParameters& params) {
target->registerFunction("string_to_binary", std::make_unique<fn_string_to_binary>(params.array_size_limit));
target->registerFunction("binary_to_string", std::make_unique<fn_binary_to_string>(params.string_size_limit));
target->registerFunction("encode_n", std::make_unique<fn_encode_n>(params.string_size_limit));
target->registerFunction("decode_n", std::make_unique<fn_decode_n>(params.string_size_limit));
return std::move(target);
}
}

+ 227
- 0
src/std/utils.cpp View File

@ -0,0 +1,227 @@
#include "UserScript.h"
#include <algorithm>
#include <span>
#include <utility>
#include "UserScriptWizardry.h"
#include "UserScriptRequire.h"
using interpreter = decltype(scripting::prepare_interpreter({}));
using namespace scripting;
namespace scripting {
struct fn_identity : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
// TODO: use the requirement sets here
if(args.size() != 1) {
errors = scripting::script_error{.message = "identity expects a single argument"};
} else {
if(std::holds_alternative<scripting::script_value>(args.front())) {
return std::get<scripting::script_value>(args.front());
} else {
return self->resolve(std::get<scripting::script_variable>(args.front()).name);
}
}
return scripting::script_value({});
}
};
struct fn_var_clear : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<0>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_clear argument error"};
} else {
self->clear_variables();
}
return scripting::script_value({});
}
};
struct fn_var_name : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<1>,
details::TypeVerifier<0, int32_t>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_name argument error"};
} else {
int32_t idx = 0;
if(std::holds_alternative<scripting::script_value>(args.front())) {
idx = std::get<int32_t>(std::get<scripting::script_value>(args.front()));
} else {
idx = std::get<int32_t>(self->resolve(std::get<scripting::script_variable>(args.front()).name));
}
if(auto val = self->varname_by_idx(idx); val) return val;
}
return scripting::script_value({});
}
};
struct fn_var_count : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<0>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_count argument error"};
} else {
return self->var_count();
}
return scripting::script_value({});
}
};
struct fn_null : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<0>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "null argument error"};
}
return scripting::script_value({});
}
};
struct fn_var_cap : public scripting::function_impl {
int32_t var_cap;
explicit fn_var_cap(int32_t _var_cap)
: var_cap(_var_cap) {}
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<0>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_cap argument error"};
return scripting::script_value({});
}
return var_cap;
}
};
struct fn_var_type : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<1>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_type argument error"};
} else {
script_value var;
if(std::holds_alternative<scripting::script_value>(args.front())) {
var = std::get<scripting::script_value>(args.front());
} else {
var = self->resolve(std::get<scripting::script_variable>(args.front()).name);
}
return std::visit(
wizardry::overloaded{
[](int32_t&) {return "integer";},
[](array&) {return "array";},
[](null&) {return "null";},
[](std::string&) {return "string";}
},
var
);
}
return scripting::script_value({});
}
};
struct fn_var_dump : public scripting::function_impl {
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
using verifier = Verify<
details::SizeEquals<1>,
details::TypeVerifier<0, int32_t>
>;
if(not verifier{}.verify(self, args)) {
errors = scripting::script_error{.message = "var_dump argument error"};
} else {
int32_t idx = 0;
if(std::holds_alternative<scripting::script_value>(args.front())) {
idx = std::get<int32_t>(std::get<scripting::script_value>(args.front()));
} else {
idx = std::get<int32_t>(self->resolve(std::get<scripting::script_variable>(args.front()).name));
}
if(auto val = self->var_by_idx(idx); val) {
return val.value();
}
}
return scripting::script_value({});
}
};
struct fn_set : public scripting::function_impl {
int32_t max_variable_count;
explicit fn_set(int32_t _max_variable_count)
: max_variable_count(_max_variable_count) {}
std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final {
// TODO: use the requirement sets here
if(args.size() != 2) {
errors = scripting::script_error{
.message = "set expects 2 arguments"
};
return scripting::script_value{};
}
auto& var = args.back();
if(not holds_alternative<scripting::script_variable>(var)) {
errors = scripting::script_error{
.message = "set expects the first argument to be a target variable"
};
return scripting::script_value{};
}
auto& arg = args.front();
if(std::holds_alternative<scripting::script_value>(arg)) {
if(self->getValue(get<scripting::script_variable>(var).name) && self->var_count() >= max_variable_count) {
errors = scripting::script_error{
.message = "set expects 2 arguments"
};
return scripting::script_value{};
}
self->setValue(get<scripting::script_variable>(var).name, std::get<scripting::script_value>(arg));
} else {
self->setValue(get<scripting::script_variable>(var).name, self->resolve(std::get<scripting::script_variable>(arg).name));
}
if(auto v = self->getValue(get<scripting::script_variable>(var).name); v) {
return v.value();
} else {
return scripting::script_value{};
}
}
};
interpreter register_utils_lib(interpreter target, const UserScriptLibraryParameters& params) {
target->registerFunction("identity", std::make_unique<fn_identity>());
target->registerFunction("var_clear", std::make_unique<fn_var_clear>());
target->registerFunction("var_name", std::make_unique<fn_var_name>());
target->registerFunction("var_count", std::make_unique<fn_var_count>());
target->registerFunction("var_cap", std::make_unique<fn_var_cap>(params.variables_count));
target->registerFunction("var_dump", std::make_unique<fn_var_dump>());
target->registerFunction("var_type", std::make_unique<fn_var_type>());
target->registerFunction("null", std::make_unique<fn_null>());
target->registerFunction("set", std::make_unique<fn_set>(params.variables_count));
return std::move(target);
}
}

+ 4
- 2
tests/parser_test.cpp View File

@ -86,6 +86,7 @@ constexpr auto runner = [](){
"../tests/scripts/005.script",
"../tests/scripts/006.script",
"../tests/scripts/007.script",
"../tests/scripts/008.script",
};
auto seed = seed_template == -1 ? std::random_device{}() : seed_template;
@ -124,7 +125,7 @@ constexpr auto runner = [](){
size_t count = 0;
size_t error_cnt = 0;
size_t success_cnt = 0;
constexpr size_t max_count = 7000000;
constexpr size_t max_count = 8000000;
auto begin = std::chrono::high_resolution_clock::now();
while(count < max_count) {
@ -168,4 +169,5 @@ TEST_CASE("Try to crash the parser (known seeds)") {
TEST_CASE("Try to crash the parser (new seeds)") {
runner<>();
}
}

+ 5
- 0
tests/scripts/008.results View File

@ -0,0 +1,5 @@
4 variables detected, printing:
answer: 42
ary: ["wizard", 7]
hello: "world"
it: 3

+ 21
- 0
tests/scripts/008.script View File

@ -0,0 +1,21 @@
/set hello "world"
/set answer 42
/set ary (/array "wizard" 7)
if(it == (/null))
/set it 0
endif
/print (/var_count) " variables detected, printing:\n"
while(it < (/var_count))
/print (/var_name it) ": "
if((/var_type (/var_dump it)) == "string")
/print "\"" (/var_dump it) "\"\n"
else
/print (/var_dump it) "\n"
endif
/set it it+1
endwhile
/exit

Loading…
Cancel
Save