added more experiments, mostly failed, see readme.
This commit is contained in:
parent
4997f9fb3d
commit
66022a0438
|
@ -3,3 +3,7 @@
|
|||
This repository contains (will contain) a bunch of unsorted interpreters for various "flavors" of lambda calculus, lisp languages and alike. It's a collection of experiments in implementing interpreters and compilers.
|
||||
|
||||
Currently, only a simple untyped lambda calculus is implemented, together with a beta reduction thing. More will be surely added in the future.
|
||||
|
||||
Typed-1 is an experiment at writing a lisp evaluator with as many type guarantees as possible, too many. You can only build the AST if everything has the correct type, which is a terrible idea and makes parsing very tricky (could not write a parser, probably just need to throw some 20 more ghc extensions at it).
|
||||
|
||||
Typed-0 is a work-in-progress regular lisp interpreter without the fancy type stuff.
|
||||
|
|
|
@ -22,6 +22,7 @@ description: Please see the README on GitHub at <https://github.com/gith
|
|||
dependencies:
|
||||
- base >= 4.7 && < 5
|
||||
- attoparsec
|
||||
- megaparsec
|
||||
- containers
|
||||
- text
|
||||
- repline
|
||||
|
@ -33,6 +34,7 @@ library:
|
|||
source-dirs:
|
||||
- src
|
||||
- src/Untyped-0
|
||||
- src/Typed-1
|
||||
|
||||
executables:
|
||||
lambdatest-exe:
|
||||
|
|
|
@ -3,18 +3,18 @@ module GenericLambda where
|
|||
|
||||
-- dumb attempt to generalize my lambda calculus functions
|
||||
|
||||
-- data instance Lambda UntypedLambda = UL UL.Environment UL.Expression UL.
|
||||
|
||||
class GenericLambda l where
|
||||
data Env l
|
||||
type Iden l
|
||||
type Data l
|
||||
data PRes l
|
||||
emptyEnv :: Env l
|
||||
updateEnv :: Env l -> Iden l -> Data l -> Env l
|
||||
envStrings :: Env l -> [(String, String)]
|
||||
normalform :: Env l -> Data l -> Either String String
|
||||
execLine :: Env l -> String -> IO (Maybe String, Env l)
|
||||
-- This typeclass encompasses a generic "lambda calculus" language, but it can be
|
||||
-- more flexible than that.
|
||||
class GenericLambda l where
|
||||
data Env l -- The type encompassing the environment, and actually the entire state of the language.
|
||||
type Iden l -- The type of identifiers.
|
||||
type Data l -- The type representing usually an expression + type information if necessary.
|
||||
data PRes l -- the type representing a parsing result.
|
||||
emptyEnv :: Env l -- the empty environment.
|
||||
updateEnv :: Env l -> Iden l -> Data l -> Env l -- function to update the environment.
|
||||
envStrings :: Env l -> [(String, String)] -- function to show the environment for tab completions.
|
||||
normalform :: Env l -> Data l -> Either String String -- function to reduce some expression.
|
||||
execLine :: Env l -> String -> IO (Maybe String, Env l) -- function to execute 1 line of "code".
|
||||
|
||||
|
||||
evalPrint' :: (GenericLambda l) => Env l -> Data l -> String
|
||||
|
|
25
src/Lib.hs
25
src/Lib.hs
|
@ -30,10 +30,10 @@ setupbuffering = do
|
|||
hSetBuffering stdout NoBuffering
|
||||
|
||||
|
||||
type Repl' a b = HaskelineT (StateT a IO) b
|
||||
type Repl a b = HaskelineT (StateT a IO) b
|
||||
|
||||
cmd' :: (GenericLambda l) => String -> Repl' (Env l) ()
|
||||
cmd' input = do
|
||||
cmd :: (GenericLambda l) => String -> Repl (Env l) ()
|
||||
cmd input = do
|
||||
env <- get
|
||||
(toprint, newenv) <- liftIO $ execLine env input
|
||||
case toprint of
|
||||
|
@ -41,8 +41,8 @@ cmd' input = do
|
|||
Just e -> liftIO $ putStrLn e
|
||||
modify (const newenv)
|
||||
|
||||
completer' :: (GenericLambda l, Monad m, MonadState (Env l) m) => CompletionFunc m
|
||||
completer' = \case
|
||||
completer :: (GenericLambda l, Monad m, MonadState (Env l) m) => CompletionFunc m
|
||||
completer = \case
|
||||
("", r) -> do
|
||||
matches <- gets (map convertmatch . envStrings)
|
||||
return ("", matches)
|
||||
|
@ -63,23 +63,20 @@ completer' = \case
|
|||
|
||||
opts = []
|
||||
|
||||
-- ini :: Repl ()
|
||||
-- ini = liftIO $ putStrLn "Type your lambda expression."
|
||||
|
||||
ini' :: (GenericLambda l) => Repl' (Env l) ()
|
||||
ini' = liftIO $ putStrLn "Type your lambda expression."
|
||||
ini :: (GenericLambda l) => Repl (Env l) ()
|
||||
ini = liftIO $ putStrLn "Type your lambda expression."
|
||||
|
||||
|
||||
final :: (GenericLambda l) => Repl' (Env l) ExitDecision
|
||||
final :: (GenericLambda l) => Repl (Env l) ExitDecision
|
||||
final = do
|
||||
liftIO $ putStrLn "Goodbye!"
|
||||
return Exit
|
||||
|
||||
|
||||
repl' :: (GenericLambda l) => IO ((), Env l)
|
||||
repl' = (runStateT $ evalRepl (const $ pure ">>> ") cmd' opts (Just ':') (Just "paste") (Custom completer') ini' final) emptyEnv
|
||||
repl :: (GenericLambda l) => IO ((), Env l)
|
||||
repl = (runStateT $ evalRepl (const $ pure ">>> ") cmd opts (Just ':') (Just "paste") (Custom completer) ini final) emptyEnv
|
||||
|
||||
uLrepl :: IO ((), Env UntypedLambda)
|
||||
uLrepl = repl'
|
||||
uLrepl = repl
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
{-# LANGUAGE GADTs #-}
|
||||
module TLispAst where
|
||||
|
||||
import qualified Data.Map as M
|
||||
|
||||
data Pos a = P {filename :: String, row:: Int, col:: Int, obj:: a}
|
||||
|
||||
|
||||
data Literal = LInt Int | Nil | LChar Char | LString String | LBool Bool
|
||||
deriving (Eq, Ord)
|
||||
|
||||
instance Show Literal where
|
||||
show (LInt i) = show i
|
||||
show Nil = "nil"
|
||||
show (LChar c) = show c
|
||||
show (LString s) = s
|
||||
show (LBool b) = show b
|
||||
|
||||
newtype Id = Id String
|
||||
|
||||
instance Show Id where
|
||||
show (Id s) = s
|
||||
|
||||
|
||||
-- data Ast where
|
||||
-- SExp :: Id -> [Ast] -> Ast
|
||||
-- SLit :: Literal -> Ast
|
||||
--
|
||||
newtype Environment = Env (M.Map Id Ast)
|
||||
data Effect = EPrint String | EError String
|
||||
|
||||
data State = S [Effect] Environment
|
||||
|
||||
addeff :: State -> Effect -> State
|
||||
addeff (S es en) e = S (e:es) en
|
||||
|
||||
instance Semigroup State where
|
||||
(<>) (S es0 env0) (S es1 env1) = S (es0 ++ es1) env1
|
||||
|
||||
|
||||
-- eval :: Environment -> Ast -> ([Effect], Environment, Ast)
|
||||
-- eval en (SLit a) = ([], en, SLit a)
|
||||
-- eval en (SExp (Id "print") )
|
||||
|
||||
|
||||
data Ast where
|
||||
Car :: Ast -> Ast
|
||||
Cdr :: Ast -> Ast
|
||||
Cons :: Ast -> Ast -> Ast
|
||||
Print :: Ast -> Ast
|
||||
Do :: [Ast] -> Ast
|
||||
Lit :: Literal -> Ast
|
||||
deriving (Eq, Show)
|
||||
|
||||
eval :: State -> Ast -> (State, Ast)
|
||||
eval s (Car a) = case eval s a of
|
||||
(s1, Cons b c) -> (s <> s1, b)
|
||||
(s1, p) -> (addeff s1 (EError ("Cannot call Car on " ++ show p)), Lit Nil)
|
||||
|
||||
eval s (Cdr a) = case eval s a of
|
||||
(s1, Cons b c) -> (s <> s1, c)
|
||||
(s1, p) -> (addeff s1 (EError ("Cannot call Cdr on " ++ show p)), Lit Nil)
|
||||
|
||||
eval s (Print a) = let (s1, k) = eval s a in
|
||||
(addeff s1 (EPrint (show k)), Lit Nil)
|
||||
|
||||
eval s (Cons a b) = (s, Cons a b)
|
||||
eval s x = (s, x)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
module TLispExec where
|
|
@ -0,0 +1 @@
|
|||
module TLispParse where
|
|
@ -0,0 +1,269 @@
|
|||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
module LispyAst where
|
||||
|
||||
import qualified Data.Map as M
|
||||
|
||||
-- -- we begin representing some primitive types.
|
||||
|
||||
|
||||
data LType = TFloat
|
||||
| TInt
|
||||
| TChar
|
||||
| TBool
|
||||
| TCons LType LType
|
||||
| TNil
|
||||
| TFun [LType] LType
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
-- -- now, we make some wrappers
|
||||
newtype Id = Id String
|
||||
deriving (Eq, Ord)
|
||||
|
||||
instance Show Id where
|
||||
show (Id s) = s
|
||||
|
||||
|
||||
|
||||
-- instance GEq (TypedId) where
|
||||
-- geq (TId i1 t1) (TId i2 t2) = Just ()
|
||||
|
||||
-- deriveGEq ''TypedId
|
||||
-- deriveGCompare ''TypedId
|
||||
-- deriveArgDict ''TypedId
|
||||
|
||||
|
||||
data Value (a :: LType) where
|
||||
LFloat :: Float -> Value TFloat
|
||||
-- LInt :: Integer -> Value TInt
|
||||
LChar :: Char -> Value TChar
|
||||
LBool :: Bool -> Value TBool
|
||||
LCons :: Value b -> Value c -> Value (TCons b c)
|
||||
Nil :: Value TNil
|
||||
LFun :: [Id] -> Value x -> Value (TFun y x)
|
||||
-- LVar :: Id -> Value (ToInfer a)
|
||||
-- deriving Show
|
||||
-- LCar :: (Value (TCons a b)) -> Value a
|
||||
-- LCdr :: (Value (TCons a b)) -> Value b
|
||||
-- there's probably a better way to do this
|
||||
-- I mean, there is probably a better way to do this entire mess
|
||||
-- but it's type safe, ain't that cool
|
||||
|
||||
-- instance Eq (Value TFloat) where
|
||||
-- (==) (LFloat f) (LFloat g) = f == g
|
||||
--
|
||||
-- instance Eq (Value TInt) where
|
||||
-- (==) (LInt a) (LInt b) = a == b
|
||||
--
|
||||
instance Num (Value TFloat) where
|
||||
(+) (LFloat a) (LFloat b) = LFloat $ a + b
|
||||
(*) (LFloat a) (LFloat b) = LFloat $ a * b
|
||||
abs (LFloat a) = LFloat $ abs a
|
||||
|
||||
instance Show (Value a) where
|
||||
show (LFloat f) = show f
|
||||
-- show (LInt i) = show i
|
||||
show (LChar c) = show c
|
||||
show (LCons a b) = "(" ++ show a ++ "," ++ show b ++ ")"
|
||||
show (LBool b) = show b
|
||||
show Nil = "()"
|
||||
show (LFun ids body) = "function: " ++ unwords (map show ids) ++ "."
|
||||
-- show (LVar (Id a)) = a
|
||||
|
||||
instance Eq (Value TFloat) where
|
||||
(==) (LFloat f) (LFloat g) = f == g
|
||||
--
|
||||
instance Ord (Value TFloat) where
|
||||
(>=) (LFloat f) (LFloat g) = f >= g
|
||||
-- (<)
|
||||
(<=) (LFloat f) (LFloat g) = f >= g
|
||||
|
||||
|
||||
data Effect = EPrint String | EError String{- | AddEnv Id [Id] SExp | Lookup Id -}
|
||||
deriving Show
|
||||
|
||||
|
||||
|
||||
data Ast a where
|
||||
IdOp :: Value a -> Ast a
|
||||
Exit :: Ast a
|
||||
Do :: [WA] -> Ast a -> Ast a
|
||||
|
||||
Print :: (Show (Value v)) => Ast v -> Ast TNil
|
||||
|
||||
Cons :: Ast a -> Ast b -> Ast (TCons a b)
|
||||
Car :: Ast (TCons x y) -> Ast x
|
||||
Cdr :: Ast (TCons x y) -> Ast y
|
||||
|
||||
-- Lambda :: [Id] -> Value x -> Ast (TFun y x)
|
||||
-- Def :: Id -> [Id] -> SExp -> Ast TNil
|
||||
|
||||
DEQ :: Value TFloat -> Value TFloat -> Ast TBool
|
||||
DLT :: Value TFloat -> Value TFloat -> Ast TBool
|
||||
DGT :: Value TFloat -> Value TFloat -> Ast TBool
|
||||
|
||||
Not :: Ast TBool -> Ast TBool
|
||||
|
||||
Sum :: Ast TFloat -> Ast TFloat -> Ast TFloat
|
||||
Sub :: Ast TFloat -> Ast TFloat -> Ast TFloat
|
||||
-- Div :: (Fractional (Value b)) => Ast b -> Ast b -> Ast b
|
||||
Mul :: Ast TFloat -> Ast TFloat -> Ast TFloat
|
||||
Abs :: Ast TFloat -> Ast TFloat
|
||||
--
|
||||
deriving instance Show (Value a) => Show (Ast a)
|
||||
-- deriving instance Num (Value a) => Num (Ast a)
|
||||
--
|
||||
|
||||
|
||||
|
||||
interpretdef :: Ast a -> ([Effect], Ast a)
|
||||
interpretdef (Print a) = ([EPrint (show a)], IdOp Nil)
|
||||
--
|
||||
interpretdef (Car (Cons a b)) = ([], a)
|
||||
interpretdef (Car a) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef (Car r1)
|
||||
in (e1 ++ e2, r2)
|
||||
interpretdef (Cdr (Cons a b)) = ([], b)
|
||||
interpretdef (Cdr a) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef (Cdr r1)
|
||||
in (e1 ++ e2, r2)
|
||||
|
||||
interpretdef (Cons a b) = ([],Cons a b)
|
||||
--
|
||||
-- interpretdef en (Def id vars body) = ([],en, IdOp Nil)
|
||||
-- interpretdef en (Lambda vars body) = ([],en, IdOp $ LFun vars body)
|
||||
--
|
||||
interpretdef (DEQ x y) = ([], IdOp $ LBool (x == y))
|
||||
interpretdef (DLT x y) = ([], IdOp $ LBool (x < y))
|
||||
interpretdef (DGT x y) = ([], IdOp $ LBool (x > y))
|
||||
--
|
||||
interpretdef (Not (IdOp (LBool b))) = ([], IdOp $ LBool (not b))
|
||||
interpretdef (Not a) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef (Not r1)
|
||||
in (e1 ++ e2, r2)
|
||||
--
|
||||
interpretdef (Sum (IdOp x) (IdOp y)) = ([], IdOp $ x + y)
|
||||
interpretdef (Sum a b) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef b
|
||||
(e3, r3) = interpretdef (Sum r1 r2)
|
||||
in (e1 ++ e2, r3)
|
||||
interpretdef (Sub (IdOp x) (IdOp y)) = ([], IdOp $ x - y)
|
||||
interpretdef (Sub a b) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef b
|
||||
(e3, r3) = interpretdef (Sub r1 r2)
|
||||
in (e1 ++ e2 ++ e3, r3)
|
||||
-- interpretdef en (Div x y) = (Nothing, x / y)
|
||||
interpretdef (Mul (IdOp x) (IdOp y)) = ([], IdOp $ x * y)
|
||||
interpretdef (Mul a b) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef b
|
||||
(e3, r3) = interpretdef (Mul r1 r2)
|
||||
in (e1 ++ e2 ++ e3, r3)
|
||||
interpretdef (Abs (IdOp x)) = ([], IdOp $ abs x)
|
||||
|
||||
interpretdef (Abs a) = let (e1, r1) = interpretdef a
|
||||
(e2, r2) = interpretdef (Abs r1)
|
||||
in (e1 ++ e2, r2)
|
||||
|
||||
interpretdef (IdOp x) = ([], IdOp x)
|
||||
interpretdef Exit = ([], Exit)
|
||||
interpretdef (Do [] end) = ([], end)
|
||||
interpretdef (Do ((WA x):xs) end) = let (e1, _) = interpretdef x
|
||||
(e2, r2) = interpretdef (Do xs end)
|
||||
in (e1 ++ e2, r2)
|
||||
|
||||
--
|
||||
|
||||
|
||||
data SExp where
|
||||
-- Builtin :: Ast (Value a) -> SExp
|
||||
Literal :: Value a -> SExp
|
||||
SExp :: Id -> [SExp] -> SExp
|
||||
--
|
||||
instance Show SExp where
|
||||
show (Literal v) = show v
|
||||
show (SExp id args) = "(" ++ show id ++ " " ++ unwords (map show args) ++ ")"
|
||||
--
|
||||
--
|
||||
-- -- wrapping the value type because existential quantification
|
||||
-- data WR = forall a. WV (Value a) {- | forall a. WD (Ast (Value a)) -}
|
||||
data WA = forall a. WA (Ast a)
|
||||
|
||||
|
||||
deriving instance Show WA
|
||||
|
||||
-- data WV = forall (a :: LType). WV (Value a)
|
||||
|
||||
--
|
||||
--
|
||||
--
|
||||
-- -- convert :: Environment -> SExp -> Maybe WValue
|
||||
-- -- convert en (Literal v) = Just $ WV v
|
||||
-- -- convert en
|
||||
--
|
||||
--
|
||||
data Environment = Env (M.Map Id SExp) Environment | Empty
|
||||
--
|
||||
instance Show Environment where
|
||||
show _ = "env."
|
||||
--
|
||||
-- lookupEnv :: Environment -> TypedId t -> Maybe (Value t)
|
||||
-- -- lookupEnv = undefined
|
||||
-- lookupEnv (Env m parent) id = case M.lookup id m of
|
||||
-- Nothing -> lookupEnv parent id
|
||||
-- Just (v) -> Just v
|
||||
lookupEnv :: Environment -> Id -> Maybe SExp
|
||||
lookupEnv (Env env parent) id = case M.lookup id env of
|
||||
Nothing -> lookupEnv parent id
|
||||
Just s -> Just s
|
||||
|
||||
lookupEnv Empty _ = Nothing
|
||||
--
|
||||
parseSexp :: Environment -> SExp -> Either String (Environment, WA)
|
||||
parseSexp en (Literal v) = Right (en, WA (IdOp v))
|
||||
parseSexp en (SExp (Id "car") [a]) = case parseSexp en a of
|
||||
Right (en', WA v@(Cons _ _)) -> Right (en', WA (Car v))
|
||||
_ -> Left "Car can only be applied to a cons cell."
|
||||
parseSexp en (SExp (Id "cdr") [a]) = case parseSexp en a of
|
||||
Right (en', WA v@(Cons _ _)) -> Right (en', WA (Cdr v))
|
||||
_ -> Left "Cdr can only be applied to a cons cell."
|
||||
parseSexp en (SExp (Id "cons") [a, b]) = case parseSexp en a of
|
||||
Right (en', WA v1) -> case parseSexp en' b of
|
||||
Right (en'', WA v2) -> Right (en'', WA (Cons v1 v2))
|
||||
Left e -> Left e
|
||||
Left e -> Left e
|
||||
-- parseSexp en (Sexp (Id "print") xs) = case
|
||||
parseSexp en (SExp (Id "+") [a, b]) = case parseSexp en a of
|
||||
Right (en', WA v1@(IdOp (LFloat _))) -> case parseSexp en' b of
|
||||
Right (en'', WA v2@(IdOp (LFloat _))) -> Right (en'', WA (Mul v1 v2))
|
||||
Right _ -> Left "Argument to + is not a float"
|
||||
Left e -> Left e
|
||||
Right _ -> Left "Argument to + is not a float"
|
||||
Left e -> Left e
|
||||
|
||||
-- parseSexp en (SExp (Id "+") (x:xs)) = case parseSexp en x of
|
||||
-- Right (en', WA v1@(IdOp (LInt _))) -> case parseSexp en' (SExp (Id "+") xs) of
|
||||
-- Right (en'', )
|
||||
|
||||
-- Right _ -> Left "Argument to + is not numeric"
|
||||
-- Left e -> Left e
|
||||
|
||||
parseSexp _ (SExp x _) = Left $ "Wrong number of arguments to " ++ show x
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
||||
-- -- interpret :: WR -> WV
|
||||
-- -- interpret (WD a) = let (res, v) = interpretdef en a in interpret (WV v)
|
||||
-- -- interpret (WV v) = WVF v
|
||||
-- -- parseSexp en (SExp id args) =
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
|
@ -0,0 +1,22 @@
|
|||
module LispyExec where
|
||||
|
||||
import LispyParser
|
||||
import LispyAst
|
||||
|
||||
test :: String -> IO ()
|
||||
test s = case runparse s of
|
||||
Left s -> putStrLn s
|
||||
Right sexp -> do
|
||||
|
||||
putStrLn $ "got a sexp:" ++ show sexp
|
||||
|
||||
case parseSexp Empty sexp of
|
||||
Left e -> putStrLn e
|
||||
Right (en, v) -> do
|
||||
-- --
|
||||
case v of
|
||||
WA v -> putStrLn $ "Got a value. here: " ++ show (interpretdef v)
|
||||
--
|
||||
--
|
||||
-- putStrLn $ interpret v
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module LispyParser where
|
||||
|
||||
import Data.Attoparsec.Text
|
||||
import LispyAst
|
||||
|
||||
import Data.String (fromString)
|
||||
import Data.Text (unpack)
|
||||
|
||||
|
||||
sexpparser :: Parser SExp
|
||||
sexpparser = choice [literalparser, parensparser]
|
||||
|
||||
identparser :: Parser Id
|
||||
identparser = Id <$> many1
|
||||
(letter <>
|
||||
char '+' <>
|
||||
char '-' <>
|
||||
char '*' <>
|
||||
char '_' <>
|
||||
char '/')
|
||||
|
||||
|
||||
literalparser = choice [nilparser, falseparser, trueparser, numberparser]
|
||||
|
||||
nilparser = do
|
||||
string "nil" <> string "()"
|
||||
return (Literal Nil)
|
||||
|
||||
falseparser = do
|
||||
string "false"
|
||||
return (Literal $ LBool False)
|
||||
|
||||
trueparser = do
|
||||
string "true"
|
||||
return (Literal $ LBool True)
|
||||
|
||||
numberparser = Literal . LFloat . fromIntegral <$> decimal
|
||||
|
||||
parensparser :: Parser SExp
|
||||
parensparser = do
|
||||
char '('
|
||||
many' space
|
||||
id <- identparser
|
||||
many' space
|
||||
sexps <- sepBy1' sexpparser (many1' space)
|
||||
char ')'
|
||||
return $ SExp id sexps
|
||||
|
||||
|
||||
runparse = parseOnly sexpparser . fromString
|
||||
|
Loading…
Reference in New Issue