Added json_write function.
M core/core/json/json_type.cpp +12 -0
@@ 142,6 142,12 @@ namespace core { namespace json {
 		return (*_data.a)[index];
 	}
 
+	void JsonType::move_push_back(JsonType &&type)
+	{
+		assert(is_array()); // expected array
+		_data.a->push_back(std::move(type));
+	}
+
 	// map methods
 
 	JsonType::MapType::const_iterator JsonType::map_begin() const

          
@@ 186,6 192,12 @@ namespace core { namespace json {
 		return (*_data.o).find(std::wstring(key)) != _data.o->end();
 	}
 
+	void JsonType::move_insert(std::wstring_view key, JsonType &&type)
+	{
+		assert(is_object()); // expected object
+		(*_data.o)[std::wstring(key)] = std::move(type);
+	}
+
 	// array or map methods
 
 	size_t JsonType::size() const

          
M core/core/json/json_type.h +45 -1
@@ 2,6 2,7 @@ 
 
 #include <map>
 #include <string>
+#include <string_view>
 #include <vector>
 
 namespace core {

          
@@ 27,6 28,9 @@ namespace core {
 		Integer,
 	};
 
+	class ArrayToken {};
+	class ObjectToken {};
+
 	/// \class JsonType
 	/// \brief This is the class of all JSON types.
 	class JsonType

          
@@ 44,6 48,44 @@ namespace core {
 			: _type_id(TypeID::None)
 		{}
 
+		explicit JsonType(bool value)
+			: _type_id(TypeID::Bool)
+		{
+			_data.b = value;
+		}
+
+		explicit JsonType(int32_t value)
+			: _type_id(TypeID::Number)
+			, _number_type(NumberType::Integer)
+		{
+			_data.i = value;
+		}
+
+		explicit JsonType(float value)
+			: _type_id(TypeID::Number)
+			, _number_type(NumberType::Float)
+		{
+			_data.f = value;
+		}
+
+		JsonType(std::wstring_view value)
+			: _type_id(TypeID::String)
+		{
+			_data.s = new std::wstring(value);
+		}
+
+		explicit JsonType(ArrayToken)
+			: _type_id(TypeID::Array)
+		{
+			_data.a = new ArrayType();
+		}
+
+		explicit JsonType(ObjectToken)
+			: _type_id(TypeID::Object)
+		{
+			_data.o = new MapType();
+		}
+
 		~JsonType() {
 			clear();
 		}

          
@@ 78,6 120,7 @@ namespace core {
 		iterator end();
 		const JsonType &operator[](size_t index) const;
 		JsonType &operator[](size_t index);
+		void move_push_back(JsonType &&type);
 
 		// map methods
 

          
@@ 88,6 131,7 @@ namespace core {
 		const JsonType &operator[](const wchar_t *key) const;
 		JsonType &operator[](const wchar_t *key);
 		bool has(std::wstring_view key) const;
+		void move_insert(std::wstring_view key, JsonType &&type);
 
 		// array or map methods
 

          
@@ 102,7 146,7 @@ namespace core {
 
 		union {
 			bool b;
-			int i;
+			int32_t i;
 			float f;
 			std::wstring *s;
 			ArrayType *a;

          
A => core/core/json/json_writer.cpp +94 -0
@@ 0,0 1,94 @@ 
+#include "pch.h"
+
+#include <core/json/json_writer.h>
+#include <format>
+#include <sstream>
+
+namespace core {
+namespace json {
+
+	void indent_line(std::basic_ostringstream<wchar_t> &output, int32_t indent)
+	{
+		if (indent < 0) {
+			return;
+		}
+		for(int32_t i = 0; i < indent; ++i) {
+			output << L'\t';
+		}
+	}
+
+	void to_string(const JsonType &value, std::basic_ostringstream<wchar_t> &output, int32_t indent)
+	{
+		if (value.is_int()) {
+			output << std::format(L"{}", value.as_int());
+
+		} else if (value.is_float()) {
+			output << std::format(L"{}", value.as_float());
+
+		} else if (value.is_string()) {
+			output << L'"' << value.as_string() << L'"';
+
+		} else if (value.is_array()) {
+			output << L'[';
+			if (indent >= 0)
+				output << L'\n';
+			if (indent >= 0)
+				++indent;
+			for(JsonType::const_iterator it = value.begin(); it != value.end(); ++it) {
+				const JsonType &item = *it;
+				indent_line(output, indent);
+				to_string(item, output, indent);
+				if (it + 1 != value.end()) {
+					output << L',';
+				}
+				if (indent >= 0)
+					output << L'\n';
+			}
+			if (indent >= 0)
+				--indent;
+			indent_line(output, indent);
+			output << L']';
+
+		} else if (value.is_object()) {
+			output << L'{';
+			if (indent >= 0)
+				output << L'\n';
+			if (indent >= 0)
+				++indent;
+			for(JsonType::MapType::const_iterator it = value.map_begin(); it != value.map_end(); ) {
+				const JsonType::MapType::value_type &pair = *it;
+				indent_line(output, indent);
+				output << L'"';
+				output << pair.first;
+				output << L"\":";
+				if (indent >= 0)
+					output << L' ';
+				to_string(pair.second, output, indent);
+				if (++it != value.map_end()) {
+					output << L',';
+				}
+				if (indent >= 0)
+					output << L'\n';
+			}
+			if (indent >= 0)
+				--indent;
+			indent_line(output, indent);
+			output << L'}';
+
+		} else if (value.is_bool()) {
+			output << (value.as_bool() ? L"true" : L"false");
+
+		} else {
+			output << L"none";
+		}
+	}
+
+	void to_string(const JsonType &type, std::wstring &output, bool human_readable)
+	{
+		std::wostringstream ss;
+		to_string(type, ss, human_readable ? 0 : -1);
+		output = ss.str();
+	}
+
+}
+}

          
A => core/core/json/json_writer.h +12 -0
@@ 0,0 1,12 @@ 
+#pragma once
+
+#include <core/json/json_type.h>
+#include <sstream>
+
+namespace core {
+namespace json {
+
+void to_string(const JsonType &value, std::wstring &output, bool human_readable);
+
+}
+}

          
M jasm/assemble/assembler_impl/assembler_impl.h +2 -0
@@ 693,7 693,9 @@ private:
 	void function_symbol(bool generate, ValueVector &expression_values, const ExpressionComponent components[], uint32_t op, uint32_t arg1);
 	void function_select(bool generate, ValueVector &expression_values, const ExpressionComponent components[], uint32_t op, uint32_t arg1, uint32_t arg2, uint32_t arg3);
 	void function_json_read(bool generate, ValueVector &expression_values, const ExpressionComponent components[], uint32_t op, uint32_t arg1);
+	void function_json_write(bool generate, ValueVector &expression_values, const ExpressionComponent components[], uint32_t op, uint32_t arg1, uint32_t arg2, uint32_t arg3);
 
+	core::json::JsonType export_json_value(const Value &value, AssemblyErrorCodes &error_code, std::string &error);
 	void import_json_value(const core::json::JsonType &json_value, Value &value);
 	void import_json_array(const core::json::JsonType &array, ListDynamicObject::ListType &output);
 	void import_json_object(const core::json::JsonType &object, DictDynamicObject::DictType &output);

          
M jasm/assemble/assembler_impl/functions_impl.cpp +124 -1
@@ 4,7 4,10 @@ 
 #include <assemble/functions.h>
 #include <cmath>
 #include <core/environment/log.h>
+#include <core/io/file_helpers.h>
+#include <core/io/file_writer.h>
 #include <core/json/json_parser.h>
+#include <core/json/json_writer.h>
 #include <core/math/sign.h>
 #include <core/strings/string_helpers.h>
 #include <core/strings/utf8.h>

          
@@ 64,6 67,7 @@ Function Assembler::function_pointer(Fun
 		{ &Assembler::function_symbol },
 		{ &Assembler::function_select },
 		{ &Assembler::function_json_read },
+		{ &Assembler::function_json_write },
 	};
 	static_assert(sizeof(pointers) / sizeof(pointers[0]) == static_cast<int>(FunctionType::NumTypes), "Number of functions doesn't match number of function pointers");
 	assert(type < FunctionType::NumTypes);

          
@@ 1567,13 1571,132 @@ void Assembler::function_json_read(bool 
 	import_json_object(object, contents);
 }
 
+void Assembler::function_json_write(bool generate, ValueVector &expression_values, const ExpressionComponent components[], uint32_t op, uint32_t arg1, uint32_t arg2, uint32_t arg3)
+{
+	if (!generate) {
+		return;
+	}
+
+	const Value &expr1_value = follow_reference_or_value(expression_values[arg1]);
+	if (!is_string(expr1_value)) {
+		const ExpressionComponent &ec = components[leftmost_node_in_expression(components, arg1)];
+		std::stringstream ss;
+		ss << "Function needs string expression argument but got " << to_string(expr1_value.type) << ".";
+		report_error(ec.source_location, AssemblyErrorCodes::ExpectedStringArgument, ss.str());
+		return;
+	}
+
+	const Value &expr2_value = follow_reference_or_value(expression_values[arg2]);
+	if (!is_dict(expr2_value)) {
+		const ExpressionComponent &ec = components[leftmost_node_in_expression(components, arg2)];
+		std::stringstream ss;
+		ss << "Function needs dict expression argument but got " << to_string(expr2_value.type) << ".";
+		report_error(ec.source_location, AssemblyErrorCodes::ExpectedDictArgument, ss.str());
+		return;
+	}
+
+	const Value &expr3_value = follow_reference_or_value(expression_values[arg3]);
+	if (!is_boolean(expr3_value)) {
+		const ExpressionComponent &ec = components[leftmost_node_in_expression(components, arg3)];
+		std::stringstream ss;
+		ss << "Function needs boolean expression argument but got " << to_string(expr3_value.type) << ".";
+		report_error(ec.source_location, AssemblyErrorCodes::ExpectedBooleanArgument, ss.str());
+		return;
+	}
+
+	// convert to json tree
+	AssemblyErrorCodes error_code = AssemblyErrorCodes::Ok;
+	std::string error;
+	core::json::JsonType object = export_json_value(expr2_value, error_code, error);
+	if (error_code != AssemblyErrorCodes::Ok)
+	{
+		const ExpressionComponent &ec = components[leftmost_node_in_expression(components, arg2)];
+		report_error(ec.source_location, error_code, error);
+		return;
+	}
+
+	// convert tree to string
+	std::wstring wide_json;
+	bool indent = dereference_boolean(expr3_value);
+	core::json::to_string(object, wide_json, indent);
+
+	// save string
+	std::string_view filename = dereference_string(expr1_value);
+	std::string utf8_json = core::wide_to_utf8(wide_json);
+
+	if (!core::save_file(std::string(filename), utf8_json, error)) {
+		const ExpressionComponent &ec = components[op];
+		std::stringstream ss;
+		ss << "Failed to write json file: " << error;
+		report_error(ec.source_location, AssemblyErrorCodes::IOError, ss.str());
+	}
+}
+
+core::json::JsonType Assembler::export_json_value(const Value &value, AssemblyErrorCodes &error_code, std::string &error)
+{
+	core::json::JsonType type;
+
+	if (is_boolean(value)) {
+		type = core::json::JsonType(dereference_boolean(value));
+
+	} else if (is_integer(value)) {
+		type = core::json::JsonType(dereference_integer(value));
+
+	} else if (is_float(value)) {
+		type = core::json::JsonType(static_cast<float>(dereference_float(value)));
+
+	} else if (is_string(value)) {
+		type = core::json::JsonType(utf8_to_wide(dereference_string(value)));
+
+	} else if (is_list(value)) {
+		const ListDynamicObject &list_object = value.object_reference.deref<ListDynamicObject>();
+		const std::vector<Value> &values = *list_object.value;
+		type = core::json::JsonType(core::json::ArrayToken());
+		for(const Value &v : values) {
+			type.move_push_back(export_json_value(v, error_code, error));
+			if (error_code != AssemblyErrorCodes::Ok) {
+				return core::json::JsonType();
+			}
+		}
+
+	} else if (is_dict(value)) {
+		const DictDynamicObject &dict_object = value.object_reference.deref<DictDynamicObject>();
+		const DictDynamicObject::DictType &values = *dict_object.value;
+		type = core::json::JsonType(core::json::ObjectToken());
+		for(const auto &pair : values) {
+			if (!is_string(pair.second.first)) {
+				std::stringstream ss;
+				ss << "Unsupported type " << to_string(pair.second.first.type) << " as key in json object.";
+				error_code = AssemblyErrorCodes::UnsupportedTypeForJsonExport;
+				error = ss.str();
+				return core::json::JsonType();
+			}
+
+			core::json::JsonType value_type = export_json_value(pair.second.second, error_code, error);
+			if (error_code != AssemblyErrorCodes::Ok) {
+				return core::json::JsonType();
+			}
+
+			type.move_insert(utf8_to_wide(dereference_string(pair.second.first)), std::move(value_type));
+		}
+
+	} else {
+		std::stringstream ss;
+		ss << "Unsupported type " << to_string(value.type) << " for json write.";
+		error_code = AssemblyErrorCodes::UnsupportedTypeForJsonExport;
+		error = ss.str();
+	}
+
+	return type;
+}
+
 void Assembler::import_json_value(const core::json::JsonType &json_value, Value &value)
 {
 	if (json_value.is_int()) {
 		set_integer(value, json_value.as_int());
 
 	} else if (json_value.is_float()) {
-		set_float(value, json_value.as_float());
+		set_float(value, double(json_value.as_float()));
 
 	} else if (json_value.is_string()) {
 		std::string value_string = core::wide_to_utf8(json_value.as_string());

          
M jasm/assemble/functions.cpp +2 -0
@@ 57,6 57,7 @@ const FunctionDesc &function_info(Functi
 		{not_lazy, fixed_args, 1}, // symbol
 		{is_lazy, fixed_args, 3}, // select
 		{not_lazy, fixed_args, 1}, // json_read
+		{not_lazy, fixed_args, 3}, // json_write
 	};
 	static_assert(sizeof(desc) / sizeof(desc[0]) == static_cast<int>(FunctionType::NumTypes), "Number of functions doesn't match number of decl structs");
 

          
@@ 112,6 113,7 @@ std::string_view to_string(FunctionType 
 		std::string_view("symbol"),
 		std::string_view("select"),
 		std::string_view("json_read"),
+		std::string_view("json_write"),
 	};
 	static_assert(sizeof(names) / sizeof(names[0]) == static_cast<size_t>(FunctionType::NumTypes), "Number of functions doesn't match number of strings");
 

          
M jasm/assemble/functions.h +1 -0
@@ 52,6 52,7 @@ enum class FunctionType : uint8_t
 	Symbol,
 	Select,
 	JsonRead,
+	JsonWrite,
 	NumTypes,
 };
 

          
M jasm/exceptions/error_codes.h +3 -0
@@ 212,6 212,9 @@ enum class AssemblyErrorCodes
 	BreakWithoutLoop,
 	FailedToIncludeJson,
 	JsonParseError,
+	ExpectedDictArgument,
+	UnsupportedTypeForJsonExport,
+	IOError,
 
 	// assembler warnings
 	SubroutineFallthrough = 3500,

          
A => jasm/unit_tests/results/test_json_export_filename_must_be_string.stdout +2 -0
@@ 0,0 1,2 @@ 
+unit_tests/test_json_export_filename_must_be_string.asm(6,13) : Error 3048 : Function needs string expression argument but got integer.
+Assembly ended with errors.

          
A => jasm/unit_tests/results/test_json_export_indent_arg_must_be_boolean.stdout +2 -0
@@ 0,0 1,2 @@ 
+unit_tests/test_json_export_indent_arg_must_be_boolean.asm(6,32) : Error 3049 : Function needs boolean expression argument but got integer.
+Assembly ended with errors.

          
A => jasm/unit_tests/results/test_json_export_type_must_be_dict.stdout +2 -0
@@ 0,0 1,2 @@ 
+unit_tests/test_json_export_type_must_be_dict.asm(6,26) : Error 3121 : Function needs dict expression argument but got boolean.
+Assembly ended with errors.

          
A => jasm/unit_tests/results/test_json_export_unsupported_type.stdout +2 -0
@@ 0,0 1,2 @@ 
+unit_tests/test_json_export_unsupported_type.asm(7,17) : Error 3122 : Unsupported type macro for json write.
+Assembly ended with errors.

          
A => jasm/unit_tests/results/test_json_export_write_error.stdout +2 -0
@@ 0,0 1,2 @@ 
+unit_tests/test_json_export_write_error.asm(6,12) : Error 3123 : Failed to write json file: Failed to open '' for writing
+Assembly ended with errors.

          
A => jasm/unit_tests/test_json_export_filename_must_be_string.asm +7 -0
@@ 0,0 1,7 @@ 
+// assembler command line arguments: 6502 [-v0]
+
+section code, "main", $8000
+{
+	const test = dict()
+	json_write(0, test, true)
+}

          
A => jasm/unit_tests/test_json_export_indent_arg_must_be_boolean.asm +7 -0
@@ 0,0 1,7 @@ 
+// assembler command line arguments: 6502 [-v0]
+
+section code, "main", $8000
+{
+	const test = dict()
+	json_write("test.json", test, 0)
+}

          
A => jasm/unit_tests/test_json_export_type_must_be_dict.asm +7 -0
@@ 0,0 1,7 @@ 
+// assembler command line arguments: 6502 [-v0]
+
+section code, "main", $8000
+{
+	const test = true
+	json_write("test.json", test, true)
+}

          
A => jasm/unit_tests/test_json_export_unsupported_type.asm +8 -0
@@ 0,0 1,8 @@ 
+// assembler command line arguments: 6502 [-v0]
+
+section code, "main", $8000
+{
+	macro m() {	}
+	const test = dict("macro" = m)
+	json_write("", test, true)
+}

          
A => jasm/unit_tests/test_json_export_write_error.asm +7 -0
@@ 0,0 1,7 @@ 
+// assembler command line arguments: 6502 [-v0]
+
+section code, "main", $8000
+{
+	const test = dict()
+	json_write("", test, true)
+}