Scriptsprache: Wie Werte repraesentieren?



  • Hallo Leute,

    Ich bastle gerade an meiner eigenen kleinen Scriptsprache herum. Das Parsing klappt soweit erstmal, nur habe ich ein Problem beim ausfuehren. Und zwar weiss ich nicht, wie ich Werte moeglichst effizient repraesentieren kann. Im Moment habe ich dafuer boost.variant verwendet:

    struct Value
    	{
    		Value()                : type_(Type::VOID),   value_()  {}
    		Value(String const& s) : type_(Type::STRING), value_(s) {}
    		Value(Char c)          : type_(Type::CHAR),   value_(c) {}
    		Value(Int i)           : type_(Type::INT),    value_(i) {}
    		Value(Float f)         : type_(Type::FLOAT),  value_(f) {}
    		Value(Bool b)          : type_(Type::BOOL),   value_(b) {}
    
    		Value& operator = (String const& s) { type_ = Type::STRING; value_ = s; return *this; }
    		Value& operator = (Char c)          { type_ = Type::CHAR;   value_ = c; return *this; }
    		Value& operator = (Int i)           { type_ = Type::INT;    value_ = i; return *this; }
    		Value& operator = (Float f)         { type_ = Type::FLOAT;  value_ = f; return *this; }
    		Value& operator = (Bool b)          { type_ = Type::BOOL;   value_ = b; return *this; }
    
    		String const& asString() const { return boost::get<String>(value_); }
    		Char            asChar() const { return boost::get<  Char>(value_); }
    		Int              asInt() const { return boost::get<   Int>(value_); }
    		Float          asFloat() const { return boost::get< Float>(value_); }
    		Bool            asBool() const { return boost::get<  Bool>(value_); }
    
    		std::string toString() const;
    
    		Type type() const { return type_; }
    		bool initialized() const { return value_.which() != 0; }
    
    	private:
    		Type type_;
    		boost::variant<Void, String, Char, Int, Float, Bool> value_;
    	};
    

    Das funktioniert mit eingebauten Typen noch gut. Jetzt will ich aber "nullable" Typen auch haben (aehnlich boost.optional), sowie Arrays, Funktionsreferenzen, Lambdas und Klassen. Und hier weiss ich nicht mehr weiter. Ich habe hier 2 verschiedene Welten: Wertypen und Referenztypen.

    Ein aehnliches Problem habe ich bei Funktionen: Sie koennen verschiedene Rueckgabetypen haben und unterschiedliche Anzahl von Parametern. Im Moment repraesentiere ich Funktionen als std::function<Value(std::vector<Value> const&)>, was unpraktisch ist, wenn man C++ Funktionen in der Scriptsprache verwenden moechte, da sie genau diese Signatur haben muessen. Z.B.:

    Value equalInt(std::vector<Value> const& v)
    		{
    			return v[0].asInt() == v[1].asInt();
    		}
    

    Was hier auch noch voellig fehlt, ist die Information, ob ein Wert ein LValue oder RValue ist.

    Habt ihr vielleicht Ideen, wie man das besser machen koennte?

    Gruesse,
    Der Kellerautomat



  • Kellerautomat schrieb:

    Und zwar weiss ich nicht, wie ich Werte moeglichst effizient repraesentieren kann. Im Moment habe ich dafuer boost.variant verwendet:

    Warum speicherst du noch einmal den Typ? variant merkt sich den doch.
    String ist vermutlich deutlich größer als die anderen (Zahlen-)typen. Das verschwendet Speicher, wenn die meisten Values gar keine Strings sind.

    Kellerautomat schrieb:

    Das funktioniert mit eingebauten Typen noch gut. Jetzt will ich aber "nullable" Typen auch haben (aehnlich boost.optional)

    Ist das nicht Void?

    Kellerautomat schrieb:

    sowie Arrays, Funktionsreferenzen, Lambdas und Klassen. Und hier weiss ich nicht mehr weiter. Ich habe hier 2 verschiedene Welten: Wertypen und Referenztypen.

    Führe eine abstrakte Klasse "Object" ein. Einer der möglichen Typen in Value ist dann Object * . Du hast vermutlich vor, einen Garbage Collector einzusetzen, also ist das mit dem Besitz der Objects kein Problem.
    String ist typischerweise von Object abgeleitet, ebenso Array, Closure, "Klassen" etc.

    Kellerautomat schrieb:

    Ein aehnliches Problem habe ich bei Funktionen: Sie koennen verschiedene Rueckgabetypen haben und unterschiedliche Anzahl von Parametern. Im Moment repraesentiere ich Funktionen als std::function<Value(std::vector<Value> const&)>, was unpraktisch ist, wenn man C++ Funktionen in der Scriptsprache verwenden moechte, da sie genau diese Signatur haben muessen. Z.B.:

    Value equalInt(std::vector<Value> const& v)
    		{
    			return v[0].asInt() == v[1].asInt();
    		}
    

    Eine Idee:

    struct Object
    {
    	virtual ~Object();
    	virtual Value call(std::vector<Value> const &arguments) const
    	{
    		throw std::runtime_error("Cannot call non-function");
    	}
    };
    
    template <class R, class ...Args>
    struct ExternalFunction : Object
    {
    	typedef std::function<R (Args...)> Function;
    
    	template <class F>
    	ExternalFunction(F &&args)
    		: m_function{std::forward<F>(args)}
    	{
    	}
    
    	virtual Value call(std::vector<Value> const &arguments) const override
    	{
    		if (arguments.size() != sizeof...(Args))
    		{
    			throw std::runtime_error("...");
    		}
    		// irgendwelches kompliziertes Gefrickel
    		// zum Konvertieren Value -> Args, das mir jetzt nicht einfällt
    		return toValue(m_function( ?? ));
    	}
    
    private:
    
    	Function m_function;
    };
    

    Man kann natürlich die std::function noch wegbekommen ( `

    template <class R, class ...Args, class Functor> struct ExternalFunction` ).



  • TyRoXx schrieb:

    Warum speicherst du noch einmal den Typ? variant merkt sich den doch.

    Da hast du wohl Recht, war mir bisher nicht so bewusst. Danke fuer den Hinweis!

    TyRoXx schrieb:

    String ist vermutlich deutlich größer als die anderen (Zahlen-)typen. Das verschwendet Speicher, wenn die meisten Values gar keine Strings sind.

    Auch das ist wohl wahr. Aber was besseres ist mir im Moment auch nicht eingefallen.

    TyRoXx schrieb:

    Kellerautomat schrieb:

    Das funktioniert mit eingebauten Typen noch gut. Jetzt will ich aber "nullable" Typen auch haben (aehnlich boost.optional)

    Ist das nicht Void?

    Nein. Void verwende ich als void-Rueckgabetyp, sowie fuer uninitialisierte Variablen.

    TyRoXx schrieb:

    Kellerautomat schrieb:

    sowie Arrays, Funktionsreferenzen, Lambdas und Klassen. Und hier weiss ich nicht mehr weiter. Ich habe hier 2 verschiedene Welten: Wertypen und Referenztypen.

    Führe eine abstrakte Klasse "Object" ein. Einer der möglichen Typen in Value ist dann Object * . Du hast vermutlich vor, einen Garbage Collector einzusetzen, also ist das mit dem Besitz der Objects kein Problem.
    String ist typischerweise von Object abgeleitet, ebenso Array, Closure, "Klassen" etc.

    Genau das wollte ich eigentlich vermeiden. Diese "Alles ist von Object abgeleitet" hierarchien finde ich einfach falsch.

    TyRoXx schrieb:

    Kellerautomat schrieb:

    Ein aehnliches Problem habe ich bei Funktionen: Sie koennen verschiedene Rueckgabetypen haben und unterschiedliche Anzahl von Parametern. Im Moment repraesentiere ich Funktionen als std::function<Value(std::vector<Value> const&)>, was unpraktisch ist, wenn man C++ Funktionen in der Scriptsprache verwenden moechte, da sie genau diese Signatur haben muessen. Z.B.:

    Value equalInt(std::vector<Value> const& v)
    		{
    			return v[0].asInt() == v[1].asInt();
    		}
    

    Eine Idee:

    struct Object
    {
    	virtual ~Object();
    	virtual Value call(std::vector<Value> const &arguments) const
    	{
    		throw std::runtime_error("Cannot call non-function");
    	}
    };
    
    template <class R, class ...Args>
    struct ExternalFunction : Object
    {
    	typedef std::function<R (Args...)> Function;
    
    	template <class F>
    	ExternalFunction(F &&args)
    		: m_function{std::forward<F>(args)}
    	{
    	}
    
    	virtual Value call(std::vector<Value> const &arguments) const override
    	{
    		if (arguments.size() != sizeof...(Args))
    		{
    			throw std::runtime_error("...");
    		}
    		// irgendwelches kompliziertes Gefrickel
    		// zum Konvertieren Value -> Args, das mir jetzt nicht einfällt
    		return toValue(m_function( ?? ));
    	}
    
    private:
    
    	Function m_function;
    };
    

    Man kann natürlich die std::function noch wegbekommen ( `

    template <class R, class ...Args, class Functor> struct ExternalFunction` ).

    Die Sprache ist aber statisch typisiert, daher macht das so keinen Sinn. Zumal ich bezweifle, dass das besonders effizient ist.



  • Hast du dir schon mal Implementationen anderer einfacher Skriptsprachen angesehen, beispielsweise TinyScheme oder Lua?

    das besonders effizient ist

    Du willst eine Scriptprache.



  • knivil schrieb:

    Hast du dir schon mal Implementationen anderer einfacher Skriptsprachen angesehen, beispielsweise TinyScheme oder Lua?

    Nein um ehrlich zu sein. Lua fand ich schon immer haesslich.

    knivil schrieb:

    das besonders effizient ist

    Du willst eine Scriptprache.

    Na und? Alle mir bekannten Scriptsprachen sind saulahm...



  • Kellerautomat schrieb:

    Na und? Alle mir bekannten Scriptsprachen sind saulahm...

    Damit meine ich, dass du dir ueber Effizienz keine Gedanken machen solltest, wenn es eine Scriptsprache sein soll.



  • Kellerautomat schrieb:

    Die Sprache ist aber statisch typisiert, daher macht das so keinen Sinn.

    Für statisch typisiert brauchst du diese Value-Klasse doch gar nicht.

    Wenn eine Funktion das erste mal ausgeführt wird (bzw. instanziiert, falls du Templates erlaubst) machst du Typchecking und kompilierst es in einen Zwischencode. Aus a+b wird intAdd a b , falls a und b Integer sind, strConcat a b falls es Strings sind etc. oder sonst TypError.

    Wenn der Zwischencode ausgeführt wird, kann stillschweigend davon ausgegangen werden, dass die Typen stimmen, dann wird keine Value-Klasse benötigt.

    Eine umfassende Value-Klasse und "alles ist von Objekt abgeleitet" sind mMn äquivalent und brauchst du genau dann, wenn du mit dynamischer Typisierung zu tun hast.



  • Mein Parser erzeugt mir einen Syntaxbaum, bei dem von Expression ablgeleitete Knoten eine virtuelle Methode evaluate(), sowie eine Methode type() haben, z.B.:

    struct LiteralExpression
    {
        virtual Type type() const override
        {
            return value_.type();
        }
    
        virtual Value evaluate() const override
        {
            return value_;
        }
    
        Value value_;
    };
    

    Fuer bessere Vorschlaege bin ich natuerlich dankbar.



  • Kellerautomat schrieb:

    Mein Parser erzeugt mir einen Syntaxbaum, bei dem von Expression ablgeleitete Knoten eine virtuelle Methode evaluate(), sowie eine Methode type() haben

    Mehr Info über deine Sprache bitte.

    Wozu brauchst du beim Ausführen den Syntaxbaum? In meiner Skriptsprache war ich auf den angewiesen wegen AST-Makros, aber sonst sehe ich dafür keinen Nutzen.

    Wenn du eine statische Typisierung hast, kannst du Überladungen auflösen, Typen checken und komplexe Ausdrücke auf einfache umformen.

    using variable_handle = int;
    using function_handle = int;
    struct expression {
      type t;
      union {
        struct assignment {
          variable_handle to; function_handle f; static_vector<variable_handle> args;
        };
        struct case { ... }; // if
        ...
    }
    
    // Der Code einer Funktion ist dann ein
    std::vector<expression>
    

    (auch funktionale Programmiersprachen können darauf reduziert werden, der Haskell-Interpreter/Compiler aus Glasgow macht das z.B.)

    Lazy Evaluation ist kein hinreichender Grund für Syntaxbäume.

    Du kannst es dir auch einfach machen und eine die statische Typisierung mit einer dynamischen zu simulieren. Dann passt dein Ansatz. Dann passe aber bitte deine Value-Klasse an und mach eine Klasse Object und lass Int/String/und so davon erben. Optional kann dann ebenfalls eine Unterklasse davon sein. Nur so kannst du übrigens int optional<optional<int> > repräsentieren, was für generischen Code nötig sein wird.

    Vererbung heisst hier natürlich nicht virtual und override sondern schön von Hand mit einer Typ-ID, und einem CRTP-Visitor mit genau einem gigantischen switch-statement. Schonmal überlegt, wie du eigene Klassen unterstützen willst? Mit deinem Ansatz kommst du da nicht weit.



  • Kellerautomat schrieb:

    knivil schrieb:

    Hast du dir schon mal Implementationen anderer einfacher Skriptsprachen angesehen, beispielsweise TinyScheme oder Lua?

    Nein um ehrlich zu sein.

    Dann tue es! Was schoen ist oder nicht, ist voellig belanglos. Hier gehts um Typrepreasentation und nicht um Sprache.


Anmelden zum Antworten