Embedding EYG in Erlang A new developer guide shows how to embed the EYG scripting language in an Erlang application named my_counters, starting with a minimal module that parses EYG source via eyg@parser:all_from_string and executes it through eyg@interpreter@expression:execute, returning {ok,{integer,3}} for the script !int_add(2, 1). The guide adds pretty-printed parser and interpreter errors with source spans and hints, and explains that pure EYG programs cannot perform side effects until the Erlang host handles algebraic effects such as Log, resuming execution with a reply value. This guide adds EYG scripting to an Erlang application my counters . Just want the code look at example ../examples/my counters erlang . Embedding EYG in Erlang gives you a scripting environment with tight controls over what can be accessed. With this control you can: - Expose scripting to the users, maybe for small queries or automations - Build a safer shell with predefined dev tasks - Provide a set of business logic functions to agents Install dependencies EYG's toolkit consists of a set of composable parts, for this guide install the following. Run a pure program Below is the simplest Erlang module that will run EYG code. -module my counters script . -export run/1 . run Code when is binary Code - case eyg@parser:all from string Code of {ok, Source} - Result = eyg@interpreter@expression:execute Source, , Result; {error, Reason} - {error, Reason} end. Start an Erlang shell, with this module loaded, and run your first scripts. counters@MyComputer 1 my counters script:run <<" int add 2, 1 " . {ok,{integer,3}} This is everything needed to run any self-contained, pure EYG program. We will add two affordances that make it easier to work with. First we will pretty print the errors. Each EYG package has its own pretty printing functions. The code below will handle formatting parser errors as well as runtime values and errors. The return value from this function is now a binary that can be presented to the user. php run Code when is binary Code - case eyg@parser:all from string Code of {ok, Source} - Result = eyg@interpreter@expression:execute Source, , case Result of {ok, Value} - debug interpreter value Value ; {error, Reason} - debug interpreter error Reason, Code end; {error, Reason} - debug parser error Reason, Code end. debug parser error Reason, Code - eyg@parser:format error Reason, Code . debug interpreter value Value - eyg@interpreter@simple debug:inspect Value . debug interpreter error {Reason, Span, , }, Code - eyg@parser:render error eyg@interpreter@simple debug:describe Reason , eyg@interpreter@simple debug:hint Reason , Code, Span . All the examples in this guide we are running from the erlang shell so it's convenient for our run function to accept charlists as well as binaries. php run Code when is list Code - run unicode:characters to binary Code ; run Code when is binary Code - % existing body Now a runtime errors prints useful output. counters@MyComputer 1 io:put chars my counters script:run <<" int add 2, foo " . error: variable undefined: foo hint: check the variable name or add a let binding before it is used 1 | int add 2, foo ^^^ Effects EYG programs in this first runner cannot perform any side effect. This is already good enough for a configuration script but for anything more we need to add effects. Running a simple hello world currently fails. perform Log "Hello, world " counters@MyComputer 1 io:put chars my counters script:run <<"perform Log \"Hello, World \" " . error: unhandled effect Log "Hello, World " hint: handle this effect or run the program in a runtime that supports it 1 | perform Log "Hello, World " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Any error interpreting an EYG program returns the reason for the break and the full program state. An unhandled effect is one type of break. For an explanation of what an algebraic effect is check out this post https://crowdhailer.me/2025-02-14/algebraic-effects-are-a-functional-approach-to-manage-side-effects/ When execution stops at an unhandled effect, the Erlang host can implement it and resume the EYG program with the reply value. We update the run function so the initial result from executing the expression is passed to a loop function that will keep running the program for every break that can be handled. php run Code when is binary Code - Output = case eyg@parser:all from string Code of {ok, Source} - Result = loop eyg@interpreter@expression:execute Source, , % same as before The implementation of loop matches on an unhandled effect . If the extrinsic implementation of the effect succeeds the loop recurses. -include lib "eyg interpreter/include/eyg@interpreter@break UnhandledEffect.hrl" . -include lib "eyg interpreter/include/eyg@interpreter@value String.hrl" . loop {error, { unhandled effect{label = Label, value = Value}, Span, Env, Stack}} - case extrinsic Label, Value of {ok, Reply} - loop eyg@interpreter@expression:resume Reply, Env, Stack ; {error, Reason} - {error, {Reason, Span, Env, Stack}} end; loop Result - Result. extrinsic <<"Log" , string{value = String} - io:put chars String , {ok, eyg@interpreter@value:unit }; extrinsic Label, Value - {error, unhandled effect{label = Label, value = Value}}. Effects can be implemented at any level of abstraction. They can be core utilities like logging or more specific to the application. The counter example has additional utilities Random and Now as well as three effects for managing the counter application. Type checking Before running an EYG we can check that it has no runtime errors. The type inferences is sound, any program that passes type checking has no runtime errors. An effect context is passed to type checking ensuring that a program has no errors including trying to call effects that are unavailable. In this type checking loop we start with a pure context and extend it with just the the Log effect. php type check Source - Infer = eyg@analysis@inference@levels j@contextual, Analysis = type check loop Infer:check inference context , Source , Infer:all errors Analysis . type check loop {done, Analysis} - Analysis; type check loop {lookup, Reference, Resume} - type check loop Resume {error, nil} . inference context - Infer = eyg@analysis@inference@levels j@contextual, Context0 = Infer:pure , Unit = eyg@analysis@type @isomorphic:record , Context1 = Infer:with effect Context0, <<"Log" , string, Unit , Context1. debug type error {Span, Reason}, Source - eyg@parser:render error eyg@analysis@type @binding@debug:render reason Reason , eyg@analysis@type @binding@debug:hint Reason , Source, Span . Replace the successful parse branch in run/1 with the following. If type checking succeeds, run the program; otherwise, return the formatted diagnostics. php {ok, Source} - case type check Source of - Result = loop eyg@interpreter@expression:execute Source, , case Result of {ok, Value} - debug interpreter value Value ; {error, Reason} - debug interpreter error Reason, Code end; Errors - unicode:characters to binary debug type error Error, Code , "\n" || Error <- Errors end; You now have an EYG scripting environment that checks programs before running them and has the full power of Erlang available for implementing effects. To expose more of your application, add effect handlers and their corresponding types to the inference context.