├── .gitignore ├── LICENCE ├── README.md ├── config └── config.exs ├── lib ├── soup.ex └── soup │ ├── ast.ex │ ├── ast │ ├── add.ex │ ├── block.ex │ ├── call.ex │ ├── eq.ex │ ├── false.ex │ ├── function.ex │ ├── if.ex │ ├── less_than.ex │ ├── let.ex │ ├── number.ex │ ├── return.ex │ ├── subtract.ex │ ├── true.ex │ └── variable.ex │ ├── cli.ex │ ├── env.ex │ ├── machine.ex │ └── source.ex ├── mix.exs ├── mix.lock ├── priv └── code │ ├── addition.soup │ ├── functions.soup │ ├── if_expression.soup │ ├── multiple_expressions.soup │ └── slow_fibonacci.soup ├── src ├── soup_parser.yrl └── soup_tokenizer.xrl └── test ├── soup ├── ast │ ├── add_test.exs │ ├── block_test.exs │ ├── call_test.exs │ ├── eq_test.exs │ ├── false_test.exs │ ├── function_test.exs │ ├── if_test.exs │ ├── less_than_test.exs │ ├── let_test.exs │ ├── number_test.exs │ ├── return_test.exs │ ├── subtract_test.exs │ ├── true_test.exs │ └── variable_test.exs ├── env_test.exs ├── machine_test.exs └── source_test.exs ├── soup_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Generated Erlang modules 20 | src/simple_tokenizer.erl 21 | src/simple_parser.erl 22 | /simple 23 | src/soup_tokenizer.erl 24 | src/soup_parser.erl 25 | /soup 26 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Soup 2 | 3 | Soup is a simple interpreted language, the runtime for which is written in 4 | Elixir. 5 | 6 | It looks like this: 7 | 8 | ```rust 9 | let x = 1 10 | let y = 2 11 | 12 | let add = |x, y| { 13 | x + y 14 | } 15 | 16 | let z = add(x, y) 17 | ``` 18 | 19 | 20 | It's largely an adaption of the Simple language from the first few chapters of 21 | Tom Stuart's excellent [Understanding Computation][book]. 22 | Go grab a copy. 23 | 24 | [book]: http://computationbook.com/ 25 | 26 | ## Usage 27 | 28 | ```sh 29 | # Compile the interpreter 30 | MIX_ENV=prod mix escript.build 31 | 32 | # Run some Souper code! 33 | ./soup priv/code/addition.soup 34 | ``` 35 | 36 | ### MPL2 Licence 37 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | if Mix.env != :prod do 4 | config :mix_test_watch, 5 | extra_extensions: [".soup"] 6 | end 7 | -------------------------------------------------------------------------------- /lib/soup.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup do 2 | @moduledoc """ 3 | A rather rubbish interpreted language. 4 | """ 5 | 6 | alias Soup.{Source, Machine} 7 | 8 | @doc """ 9 | Makes the magic happen. 10 | """ 11 | def eval(source) when is_binary(source) do 12 | {:ok, machine} = 13 | source 14 | |> Source.parse!() 15 | |> Machine.new() 16 | |> Machine.run 17 | machine.ast 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/soup/ast.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST do 2 | @type t :: any 3 | 4 | alias Soup.Env 5 | 6 | @spec reduce(t, Env.t) :: {:ok, t, Env.t} | :noop 7 | def reduce(ast, env) do 8 | case Soup.AST.Protocol.reduce(ast, env) do 9 | {:ok, _, %Env{}} = result -> 10 | result 11 | 12 | :noop -> 13 | :noop 14 | end 15 | end 16 | 17 | @spec to_source(t, any) :: String.t 18 | def to_source(expr, opts \\ %{}) do 19 | Soup.AST.Protocol.to_source(expr, opts) 20 | end 21 | end 22 | 23 | 24 | defprotocol Soup.AST.Protocol do 25 | alias Soup.{AST, Env} 26 | 27 | @spec reduce(AST.t, Env.t) :: {:ok, AST.t, Env.t} | :noop 28 | def reduce(data, env) 29 | 30 | @spec to_source(AST.t, any) :: String.t 31 | def to_source(expr, opts) 32 | end 33 | -------------------------------------------------------------------------------- /lib/soup/ast/add.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Add do 2 | @moduledoc """ 3 | Number addition. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:lhs, :rhs] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @type t :: %__MODULE__{lhs: AST.t, rhs: AST.t} 13 | 14 | @doc """ 15 | Construct an Add node. 16 | 17 | ...> Soup.Add.new(Soup.Number.new(1), Soup.Number.new(2)) 18 | %Soup.Add{lhs: Number.new(1), rhs: Number.new(2)} 19 | """ 20 | @spec new(struct, struct) :: t 21 | def new(lhs, rhs) when is_map(lhs) and is_map(rhs) do 22 | %__MODULE__{lhs: lhs, rhs: rhs} 23 | end 24 | end 25 | 26 | defimpl Soup.AST.Protocol, for: Soup.AST.Add do 27 | 28 | def to_source(%{lhs: lhs, rhs: rhs}, _opts) do 29 | import Soup.AST, only: [to_source: 1] 30 | "#{to_source lhs} + #{to_source rhs}" 31 | end 32 | 33 | def reduce(%{lhs: lhs, rhs: rhs}, env) do 34 | alias Soup.AST 35 | alias Soup.AST.{Add, Number} 36 | with {:lhs, :noop} <- {:lhs, AST.reduce(lhs, env)}, 37 | {:rhs, :noop} <- {:rhs, AST.reduce(rhs, env)} do 38 | new_num = Number.new(lhs.value + rhs.value) 39 | {:ok, new_num, env} 40 | else 41 | {:lhs, {:ok, new_lhs, new_env}} -> 42 | {:ok, Add.new(new_lhs, rhs), new_env} 43 | 44 | {:rhs, {:ok, new_rhs, new_env}} -> 45 | {:ok, Add.new(lhs, new_rhs), new_env} 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/soup/ast/block.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Block do 2 | @moduledoc """ 3 | A block of multiple expressions. 4 | """ 5 | 6 | keys = [:expressions] 7 | @enforce_keys keys 8 | defstruct keys 9 | 10 | @type t :: %__MODULE__{expressions: [AST.t]} 11 | 12 | @doc """ 13 | Construct a Block node. 14 | 15 | iex> Soup.AST.Block.new([Soup.AST.Number.new(1)]) 16 | %Soup.AST.Block{expressions: [Soup.AST.Number.new(1)]} 17 | """ 18 | @spec new(number) :: t 19 | def new(exprs) when is_list(exprs) do 20 | %__MODULE__{expressions: exprs} 21 | end 22 | end 23 | 24 | defimpl Soup.AST.Protocol, for: Soup.AST.Block do 25 | 26 | def reduce(%{expressions: [expr]}, env) do 27 | case Soup.AST.reduce(expr, env) do 28 | {:ok, new_expr, new_env} -> 29 | new_block = Soup.AST.Block.new([new_expr]) 30 | {:ok, new_block, new_env} 31 | 32 | :noop -> 33 | {:ok, expr, env} 34 | end 35 | end 36 | 37 | def reduce(%{expressions: [expr|tail_exprs]}, env) do 38 | case Soup.AST.reduce(expr, env) do 39 | {:ok, new_expr, new_env} -> 40 | new_block = Soup.AST.Block.new([new_expr|tail_exprs]) 41 | {:ok, new_block, new_env} 42 | 43 | :noop -> 44 | new_block = Soup.AST.Block.new(tail_exprs) 45 | {:ok, new_block, env} 46 | end 47 | end 48 | 49 | def to_source(%{expressions: expressions}, _opts) do 50 | expressions 51 | |> Enum.map(&Soup.AST.to_source/1) 52 | |> Enum.join("\n") 53 | |> Kernel.<>("\n") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/soup/ast/call.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Call do 2 | @moduledoc """ 3 | Value assignment. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:function, :arguments] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @type t :: %__MODULE__{function: atom, arguments: AST.t} 13 | 14 | @doc """ 15 | Construct a function call node. 16 | 17 | iex> Soup.AST.Call.new(:print, [Soup.AST.Number.new(50)]) 18 | %Soup.AST.Call{function: :print, arguments: [Soup.AST.Number.new(50)]} 19 | """ 20 | @spec new(atom, [AST.t]) :: t 21 | def new(function, arguments) 22 | when is_atom(function) and is_list(arguments) 23 | do 24 | %__MODULE__{function: function, arguments: arguments} 25 | end 26 | end 27 | 28 | 29 | defimpl Soup.AST.Protocol, for: Soup.AST.Call do 30 | alias Soup.{AST, Env} 31 | 32 | def reduce(call, env) do 33 | with :noop <- reduce_arguments(call.arguments, env), 34 | {:ok, function} <- Env.get(env, call.function), 35 | {:ok, new_env} <- env 36 | |> Env.push_stack() 37 | |> Env.put(call.function, function) 38 | |> prep_scope(function.arguments, call.arguments) 39 | do 40 | {:ok, AST.Return.new(function.body), new_env} 41 | else 42 | :not_set -> 43 | throw {:undefined_function, call.function} 44 | 45 | :invalid_arity -> 46 | throw {:invalid_arity, call.function} 47 | 48 | {:arg_reduced, new_args, new_env} -> 49 | {:ok, %{call | arguments: new_args}, new_env} 50 | 51 | end 52 | end 53 | 54 | defp prep_scope(env, [name|ns], [value|vs]) do 55 | env |> Env.put(name, value) |> prep_scope(ns, vs) 56 | end 57 | defp prep_scope(env, [], []) do 58 | {:ok, env} 59 | end 60 | defp prep_scope(_, _, _) do 61 | :invalid_arity 62 | end 63 | 64 | defp reduce_arguments(args, env) when is_list(args) do 65 | cannot_reduce = fn(x) -> AST.reduce(x, env) == :noop end 66 | {noops, reducibles} = Enum.split_while(args, cannot_reduce) 67 | case reducibles do 68 | [] -> 69 | :noop 70 | 71 | [next|rest] -> 72 | {:ok, reduced, new_env} = AST.reduce(next, env) 73 | new_args = noops ++ [reduced|rest] 74 | {:arg_reduced, new_args, new_env} 75 | end 76 | end 77 | 78 | def to_source(%{function: function, arguments: arguments}, _opts) do 79 | args = arguments |> Enum.map(&Soup.AST.to_source/1) |> Enum.join(", ") 80 | "#{function}(#{args})" 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/soup/ast/eq.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Eq do 2 | @moduledoc """ 3 | Equality comparison. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:lhs, :rhs] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @type t :: %__MODULE__{lhs: AST.t, rhs: AST.t} 13 | 14 | @doc """ 15 | Construct an Eq node. 16 | 17 | ...> Soup.Eq.new(Soup.Number.new(1), Soup.Number.new(2)) 18 | %Soup.Eq{lhs: Number.new(1), rhs: Number.new(2)} 19 | """ 20 | @spec new(struct, struct) :: t 21 | def new(lhs, rhs) when is_map(lhs) and is_map(rhs) do 22 | %__MODULE__{lhs: lhs, rhs: rhs} 23 | end 24 | end 25 | 26 | defimpl Soup.AST.Protocol, for: Soup.AST.Eq do 27 | alias Soup.AST 28 | alias Soup.AST.{Eq, True, False} 29 | 30 | def to_source(%{lhs: lhs, rhs: rhs}, _opts) do 31 | "#{AST.to_source lhs} == #{AST.to_source rhs}" 32 | end 33 | 34 | def reduce(%{lhs: lhs, rhs: rhs}, env) do 35 | with {:lhs, :noop} <- {:lhs, AST.reduce(lhs, env)}, 36 | {:rhs, :noop} <- {:rhs, AST.reduce(rhs, env)} do 37 | bool = if lhs.value == rhs.value do 38 | True.new 39 | else 40 | False.new 41 | end 42 | {:ok, bool, env} 43 | else 44 | {:lhs, {:ok, new_lhs, new_env}} -> 45 | {:ok, Eq.new(new_lhs, rhs), new_env} 46 | 47 | {:rhs, {:ok, new_rhs, new_env}} -> 48 | {:ok, Eq.new(lhs, new_rhs), new_env} 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/soup/ast/false.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.False do 2 | defstruct [] 3 | 4 | @type t :: %__MODULE__{} 5 | 6 | @doc """ 7 | Construct a false boolean node. 8 | 9 | iex> Soup.AST.False.new() 10 | %Soup.AST.False{} 11 | """ 12 | @spec new() :: t 13 | def new do 14 | %__MODULE__{} 15 | end 16 | end 17 | 18 | defimpl Soup.AST.Protocol, for: Soup.AST.False do 19 | 20 | def reduce(_, _) do 21 | :noop 22 | end 23 | 24 | def to_source(%{}, _opts \\ nil) do 25 | "false" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/soup/ast/function.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Function do 2 | @moduledoc """ 3 | A number. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:arguments, :body] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @type t :: %__MODULE__{arguments: [atom], body: AST.t} 13 | 14 | @doc """ 15 | Construct a Function node. 16 | 17 | iex> Soup.AST.Function.new([:_], Soup.AST.Number.new(1)) 18 | %Soup.AST.Function{arguments: [:_], body: Soup.AST.Number.new(1)} 19 | """ 20 | @spec new([atom], AST.t) :: t 21 | def new(arguments, body) when is_list(arguments) and is_map(body) do 22 | %__MODULE__{arguments: arguments, body: body} 23 | end 24 | end 25 | 26 | defimpl Soup.AST.Protocol, for: Soup.AST.Function do 27 | 28 | def reduce(_, _) do 29 | :noop 30 | end 31 | 32 | def to_source(%{arguments: arguments, body: body}, _opts) do 33 | import Soup.AST, only: [to_source: 1] 34 | """ 35 | |#{Enum.join(arguments, ", ")}| { 36 | #{to_source(body)} 37 | } 38 | """ 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/soup/ast/if.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.If do 2 | keys = [:condition, :consequence, :alternative] 3 | @enforce_keys keys 4 | defstruct keys 5 | 6 | alias Soup.AST 7 | 8 | @type t :: %__MODULE__{condition: AST.t, 9 | consequence: AST.t, 10 | alternative: AST.t} 11 | 12 | @doc """ 13 | Construct an if flow control node. 14 | """ 15 | @spec new(AST.t, AST.t, AST.t) :: t 16 | def new(condition, consequence, alternative) do 17 | %__MODULE__{condition: condition, 18 | consequence: consequence, 19 | alternative: alternative} 20 | end 21 | end 22 | 23 | defimpl Soup.AST.Protocol, for: Soup.AST.If do 24 | 25 | def to_source(x, _opts) do 26 | import Soup.AST, only: [to_source: 1] 27 | """ 28 | if #{to_source x.condition} { 29 | #{to_source x.consequence} 30 | } else { 31 | #{to_source x.alternative} 32 | } 33 | """ 34 | end 35 | 36 | def reduce(%{condition: %Soup.AST.False{}} = x, env) do 37 | {:ok, x.alternative, env} 38 | end 39 | def reduce(x, env) do 40 | case Soup.AST.reduce(x.condition, env) do 41 | :noop -> 42 | {:ok, x.consequence, env} 43 | {:ok, condition, new_env} -> 44 | new_if = Soup.AST.If.new(condition, x.consequence, x.alternative) 45 | {:ok, new_if, new_env} 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/soup/ast/less_than.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.LessThan do 2 | keys = [:lhs, :rhs] 3 | @enforce_keys keys 4 | defstruct keys 5 | 6 | alias Soup.AST 7 | alias Soup.AST.True 8 | 9 | @type t :: %__MODULE__{lhs: AST.t, 10 | rhs: AST.t} 11 | 12 | @doc """ 13 | Construct an size comparison node. 14 | """ 15 | @spec new(AST.t, AST.t) :: t 16 | def new(lhs, rhs) do 17 | %__MODULE__{lhs: lhs, rhs: rhs} 18 | end 19 | end 20 | 21 | defimpl Soup.AST.Protocol, for: Soup.AST.LessThan do 22 | 23 | def to_source(x, _opts) do 24 | import Soup.AST, only: [to_source: 1] 25 | "#{to_source x.lhs} < #{to_source x.rhs}" 26 | end 27 | 28 | def reduce(%{lhs: lhs, rhs: rhs}, env) do 29 | alias Soup.AST 30 | alias Soup.AST.{LessThan, True, False} 31 | with {:lhs, :noop} <- {:lhs, AST.reduce(lhs, env)}, 32 | {:rhs, :noop} <- {:rhs, AST.reduce(rhs, env)} do 33 | {:ok, compare(lhs, rhs), env} 34 | else 35 | {:lhs, {:ok, new_lhs, new_env}} -> {:ok, LessThan.new(new_lhs, rhs), new_env} 36 | {:rhs, {:ok, new_rhs, new_env}} -> {:ok, LessThan.new(lhs, new_rhs), new_env} 37 | end 38 | end 39 | 40 | defp compare(lhs, rhs) do 41 | alias Soup.AST.{True, False} 42 | if lhs < rhs do 43 | True.new() 44 | else 45 | False.new() 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/soup/ast/let.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Let do 2 | @moduledoc """ 3 | Value assignment. 4 | """ 5 | 6 | keys = [:name, :value] 7 | @enforce_keys keys 8 | defstruct keys 9 | 10 | @type t :: %__MODULE__{name: atom, value: AST.t} 11 | 12 | @doc """ 13 | Construct a Let node. 14 | 15 | iex> Soup.AST.Let.new(:x, Soup.AST.Number.new(64)) 16 | %Soup.AST.Let{name: :x, value: Soup.AST.Number.new(64)} 17 | """ 18 | @spec new(atom, AST.t) :: t 19 | def new(name, value) when is_atom(name) and is_map(value) do 20 | %__MODULE__{name: name, value: value} 21 | end 22 | end 23 | 24 | 25 | defimpl Soup.AST.Protocol, for: Soup.AST.Let do 26 | 27 | def reduce(%{name: name, value: value}, env) do 28 | alias Soup.AST 29 | case AST.reduce(value, env) do 30 | :noop -> 31 | new_env = Soup.Env.put(env, name, value) 32 | {:ok, value, new_env} 33 | 34 | {:ok, new_value, env} -> 35 | {:ok, AST.Let.new(name, new_value), env} 36 | end 37 | end 38 | 39 | def to_source(%{name: name, value: value}, _opts) do 40 | n = to_string(name) 41 | v = Soup.AST.to_source(value) 42 | "let #{n} = #{v}" 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/soup/ast/number.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Number do 2 | @moduledoc """ 3 | A number. 4 | """ 5 | 6 | keys = [:value] 7 | @enforce_keys keys 8 | defstruct keys 9 | 10 | @type t :: %__MODULE__{value: number} 11 | 12 | @doc """ 13 | Construct a Number node. 14 | 15 | iex> Soup.AST.Number.new(64) 16 | %Soup.AST.Number{value: 64} 17 | """ 18 | @spec new(number) :: t 19 | def new(n) when is_number(n) do 20 | %__MODULE__{value: n} 21 | end 22 | end 23 | 24 | defimpl Soup.AST.Protocol, for: Soup.AST.Number do 25 | 26 | def reduce(_, _) do 27 | :noop 28 | end 29 | 30 | def to_source(%{value: value}, _opts) do 31 | to_string(value) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/soup/ast/return.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Return do 2 | @moduledoc """ 3 | A marker for when a function returned. Used for stack manipulation. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:value] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @opaque t :: %__MODULE__{value: AST.t} 13 | 14 | @doc """ 15 | Construct a Return node. 16 | 17 | iex> Soup.AST.Return.new(Soup.AST.Number.new(3)) 18 | %Soup.AST.Return{value: Soup.AST.Number.new(3)} 19 | """ 20 | @spec new(AST.t) :: t 21 | def new(value) when is_map(value) do 22 | %__MODULE__{value: value} 23 | end 24 | end 25 | 26 | defimpl Soup.AST.Protocol, for: Soup.AST.Return do 27 | 28 | alias Soup.{AST, Env} 29 | 30 | def reduce(return, env) do 31 | case AST.reduce(return.value, env) do 32 | {:ok, new_value, env} -> 33 | new_ret = AST.Return.new(new_value) 34 | {:ok, new_ret, env} 35 | 36 | :noop -> 37 | {:ok, return.value, Env.pop_stack(env)} 38 | end 39 | end 40 | 41 | def to_source(%{value: value}, _opts) do 42 | Soup.AST.to_source(value) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/soup/ast/subtract.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Subtract do 2 | @moduledoc """ 3 | Number subtraction. 4 | """ 5 | 6 | alias Soup.AST 7 | 8 | keys = [:lhs, :rhs] 9 | @enforce_keys keys 10 | defstruct keys 11 | 12 | @type t :: %__MODULE__{lhs: AST.t, rhs: AST.t} 13 | 14 | @doc """ 15 | Construct an Subtract node. 16 | 17 | ...> Soup.Subtract.new(Soup.Number.new(1), Soup.Number.new(2)) 18 | %Soup.Subtract{lhs: Number.new(1), rhs: Number.new(2)} 19 | """ 20 | @spec new(struct, struct) :: t 21 | def new(lhs, rhs) when is_map(lhs) and is_map(rhs) do 22 | %__MODULE__{lhs: lhs, rhs: rhs} 23 | end 24 | end 25 | 26 | defimpl Soup.AST.Protocol, for: Soup.AST.Subtract do 27 | 28 | def to_source(%{lhs: lhs, rhs: rhs}, _opts) do 29 | import Soup.AST, only: [to_source: 1] 30 | "#{to_source lhs} - #{to_source rhs}" 31 | end 32 | 33 | def reduce(%{lhs: lhs, rhs: rhs}, env) do 34 | alias Soup.AST 35 | alias Soup.AST.{Subtract, Number} 36 | with {:lhs, :noop} <- {:lhs, AST.reduce(lhs, env)}, 37 | {:rhs, :noop} <- {:rhs, AST.reduce(rhs, env)} do 38 | new_num = Number.new(lhs.value - rhs.value) 39 | {:ok, new_num, env} 40 | else 41 | {:lhs, {:ok, new_lhs, new_env}} -> 42 | {:ok, Subtract.new(new_lhs, rhs), new_env} 43 | 44 | {:rhs, {:ok, new_rhs, new_env}} -> 45 | {:ok, Subtract.new(lhs, new_rhs), new_env} 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/soup/ast/true.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.True do 2 | defstruct [] 3 | 4 | @type t :: %__MODULE__{} 5 | 6 | @doc """ 7 | Construct a true boolean node. 8 | 9 | iex> Soup.AST.True.new() 10 | %Soup.AST.True{} 11 | """ 12 | @spec new() :: t 13 | def new do 14 | %__MODULE__{} 15 | end 16 | end 17 | 18 | defimpl Soup.AST.Protocol, for: Soup.AST.True do 19 | 20 | def reduce(_, _) do 21 | :noop 22 | end 23 | 24 | def to_source(%{}, _opts) do 25 | "true" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/soup/ast/variable.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.Variable do 2 | @moduledoc """ 3 | Value assignment. 4 | """ 5 | 6 | keys = [:name] 7 | @enforce_keys keys 8 | defstruct keys 9 | 10 | @type t :: %__MODULE__{name: atom} 11 | 12 | @doc """ 13 | Construct a Let node. 14 | 15 | iex> Soup.AST.Variable.new(:x) 16 | %Soup.AST.Variable{name: :x} 17 | """ 18 | @spec new(atom) :: t 19 | def new(name) when is_atom(name) do 20 | %__MODULE__{name: name} 21 | end 22 | end 23 | 24 | 25 | defimpl Soup.AST.Protocol, for: Soup.AST.Variable do 26 | 27 | def reduce(%{name: name}, env) do 28 | alias Soup.Env 29 | case Env.get(env, name) do 30 | :not_set -> throw {:undefined_variable, name} 31 | {:ok, v} -> {:ok, v, env} 32 | end 33 | end 34 | 35 | def to_source(%{name: name}, _opts) do 36 | to_string(name) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/soup/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.CLI do 2 | @moduledoc """ 3 | Command line interface to Soup.ex 4 | """ 5 | 6 | def main([path]) do 7 | path 8 | |> File.read!() 9 | |> Soup.eval() 10 | |> IO.inspect() 11 | end 12 | 13 | def main(_) do 14 | IO.puts """ 15 | USAGE: soup path/to/code.smp 16 | """ 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/soup/env.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.Env do 2 | @typep scope :: %{optional(atom) => any} 3 | @opaque t :: %__MODULE__{stack: [scope]} 4 | 5 | defstruct stack: [%{}] 6 | 7 | 8 | @doc """ 9 | Construct a new Env. 10 | 11 | """ 12 | def new do 13 | %__MODULE__{} 14 | end 15 | 16 | 17 | @doc """ 18 | Set a value in the current scope. 19 | 20 | """ 21 | def put(%__MODULE__{} = env, name, value) 22 | when is_atom(name) and is_map(value) do 23 | [scope|scopes] = env.stack 24 | new_scope = Map.put(scope, name, value) 25 | %{env | stack: [new_scope|scopes]} 26 | end 27 | 28 | 29 | @doc """ 30 | Get a value in the current scope. 31 | 32 | """ 33 | def get(%__MODULE__{stack: [scope|_]}, name) 34 | when is_atom(name) do 35 | case Map.fetch(scope, name) do 36 | :error -> :not_set 37 | {:ok, _} = x -> x 38 | end 39 | end 40 | 41 | 42 | @doc """ 43 | Push a fresh scope onto the stack 44 | 45 | """ 46 | def push_stack(%__MODULE__{} = env) do 47 | %{env | stack: [%{}|env.stack]} 48 | end 49 | 50 | 51 | @doc """ 52 | Pop the current scope off the stack 53 | 54 | """ 55 | def pop_stack(%__MODULE__{} = env) do 56 | %{env | stack: tl(env.stack)} 57 | end 58 | 59 | @doc """ 60 | Get the size of the stack. 61 | 62 | iex> Soup.Env.new() |> Soup.Env.push_stack() |> Soup.Env.stack_size() 63 | 2 64 | """ 65 | def stack_size(%__MODULE__{} = env) do 66 | length(env.stack) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/soup/machine.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.Machine do 2 | 3 | alias Soup.{Env, AST} 4 | 5 | @opaque t :: %__MODULE__{ast: AST.t, env: Env.t} 6 | 7 | @enforce_keys [:ast] 8 | defstruct ast: nil, 9 | env: Env.new 10 | 11 | @doc """ 12 | Construct a new Machine from a given AST. 13 | """ 14 | def new(ast, env \\ Env.new) when is_map(ast) and is_map(env) do 15 | %__MODULE__{ast: ast, env: env} 16 | end 17 | 18 | 19 | @doc """ 20 | Attempt to reduce the state of the machine by one step. 21 | """ 22 | @spec step(t) :: {:ok, t} | :noop 23 | def step(%__MODULE__{ast: ast, env: env}) do 24 | case AST.reduce(ast, env) do 25 | {:ok, new_ast, new_env} -> {:ok, new(new_ast, new_env)} 26 | 27 | :noop -> :noop 28 | end 29 | end 30 | 31 | @doc """ 32 | Reduce the state of the machine until it can be reduced no more. 33 | """ 34 | @spec run(t) :: {:ok, t} 35 | def run(%__MODULE__{} = machine) do 36 | case step(machine) do 37 | :noop -> {:ok, machine} 38 | 39 | {:ok, new_machine} -> run(new_machine) 40 | end 41 | end 42 | end 43 | 44 | defimpl Inspect, for: Soup.Machine do 45 | def inspect(_, _) do 46 | "#Soup.Machine" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/soup/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Soup.Source do 2 | @moduledoc """ 3 | Source code parsing and tokenization. 4 | """ 5 | 6 | @type token :: {atom, any, any} 7 | 8 | @doc """ 9 | Convert a string of source code into tokens. 10 | """ 11 | @spec tokenize(String.t) :: {:ok, [token]} | {:error, number, tuple} 12 | def tokenize(source) when is_binary(source) do 13 | source 14 | |> String.to_charlist() 15 | |> :soup_tokenizer.string() 16 | |> case do 17 | {:ok, tokens, _} -> 18 | {:ok, tokens} 19 | 20 | {:error, {line, _, error}, _} -> 21 | {:error, line, error} 22 | end 23 | end 24 | 25 | @doc """ 26 | Convert a string of source code into tokens. 27 | 28 | Throws on syntax error. 29 | 30 | TODO: Better error messages. 31 | """ 32 | @spec tokenize(String.t) :: [token] 33 | def tokenize!(source) do 34 | {:ok, source} = Soup.Source.tokenize(source) 35 | source 36 | end 37 | 38 | 39 | @doc """ 40 | Converts a string of source code into an AST. 41 | """ 42 | @spec tokenize(String.t) :: {:ok, AST.t} 43 | def parse(source) when is_binary(source) do 44 | case tokenize(source) do 45 | {:ok, tokens} -> 46 | :soup_parser.parse(tokens) 47 | 48 | {:error, _, _} = error -> 49 | error 50 | end 51 | end 52 | 53 | @doc """ 54 | Converts a string of source code into an AST. 55 | 56 | Throws on syntax error. 57 | 58 | TODO: Better error messages. 59 | """ 60 | def parse!(source) do 61 | {:ok, ast} = Soup.Source.parse(source) 62 | ast 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :soup, 6 | version: "0.1.0", 7 | elixir: "~> 1.3", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | escript: [main_module: Soup.CLI], 11 | deps: deps()] 12 | end 13 | 14 | def application do 15 | [applications: [:logger]] 16 | end 17 | 18 | defp deps do 19 | [{:mix_test_watch, "~> 0.2.0", only: [:dev, :test]}, 20 | {:dialyxir, "~> 0.4", only: [:dev]}] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"dialyxir": {:hex, :dialyxir, "0.4.3", "a4daeebd0107de10d3bbae2ccb6b8905e69544db1ed5fe9148ad27cd4cb2c0cd", [:mix], []}, 2 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 3 | "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}} 4 | -------------------------------------------------------------------------------- /priv/code/addition.soup: -------------------------------------------------------------------------------- 1 | 1 + 2 + 3 2 | // vim: set ft=rust: 3 | -------------------------------------------------------------------------------- /priv/code/functions.soup: -------------------------------------------------------------------------------- 1 | let identity = |a| { 2 | a 3 | } 4 | 5 | let double = |a| { 6 | a + a 7 | } 8 | 9 | let negate = |a| { 10 | 0 - a 11 | } 12 | 13 | let twice = |function, a| { 14 | function(function(a)) 15 | } 16 | 17 | // vim: set ft=rust: 18 | -------------------------------------------------------------------------------- /priv/code/if_expression.soup: -------------------------------------------------------------------------------- 1 | let z = if 1 < 2 { 2 | 100 3 | } else { 4 | 200 5 | } 6 | 7 | // vim: set ft=rust: 8 | -------------------------------------------------------------------------------- /priv/code/multiple_expressions.soup: -------------------------------------------------------------------------------- 1 | let a = 1 2 | let b = 2 3 | let c = 3 4 | // vim: set ft=rust: 5 | -------------------------------------------------------------------------------- /priv/code/slow_fibonacci.soup: -------------------------------------------------------------------------------- 1 | // 2 | // Inefficient recursive fibonacci 3 | // 4 | // This is the first Soup program ever run. Fun fun! 5 | // 6 | let fibonacci = |x| { 7 | if x == 0 { 8 | 0 9 | } else { 10 | if x == 1 { 11 | 1 12 | } else { 13 | fibonacci(x - 1) + fibonacci(x - 2) 14 | } 15 | } 16 | } 17 | 18 | fibonacci(10) 19 | 20 | // vim: set ft=rust: 21 | -------------------------------------------------------------------------------- /src/soup_parser.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals 2 | expressions expression literal 3 | fn_arguments call_arguments. 4 | 5 | Terminals 6 | '+' '-' '<' '==' 7 | '{' '}' '(' ')' 8 | '|' ',' 9 | number 10 | true false 11 | atom 12 | let '=' 13 | if else. 14 | 15 | Rootsymbol expressions. 16 | 17 | Left 300 '+'. 18 | Left 300 '-'. 19 | Right 100 '<'. 20 | Nonassoc 200 '=='. 21 | 22 | expressions -> expression : mk_block('$1'). 23 | expressions -> expression expressions : mk_block('$1', '$2'). 24 | 25 | expression -> literal : '$1'. 26 | expression -> atom : mk_variable('$1'). 27 | expression -> expression '==' expression : mk_eq('$1', '$3'). 28 | expression -> expression '-' expression : mk_subtract('$1', '$3'). 29 | expression -> expression '+' expression : mk_add('$1', '$3'). 30 | expression -> expression '<' expression : mk_less_than('$1', '$3'). 31 | expression -> let atom '=' expression : mk_let('$2', '$4'). 32 | expression -> atom '(' call_arguments ')' : mk_call('$1', '$3'). 33 | 34 | % if (x) { 1 } else { 2 } 35 | expression -> if expression '{' expressions '}' 36 | else '{' expressions '}' : mk_if('$2', '$4', '$8'). 37 | 38 | % |x, y| { x + y } 39 | expression -> '|' fn_arguments '|' 40 | '{' expressions '}' : mk_function('$2', '$5'). 41 | 42 | call_arguments -> expression : mk_call_arguments('$1', []). 43 | call_arguments -> expression ',' call_arguments : mk_call_arguments('$1', '$3'). 44 | 45 | fn_arguments -> atom : mk_fn_arguments('$1', []). 46 | fn_arguments -> atom ',' fn_arguments : mk_fn_arguments('$1', '$3'). 47 | 48 | literal -> number : mk_number('$1'). 49 | literal -> true : mk_true('$1'). 50 | literal -> false : mk_false('$1'). 51 | 52 | Erlang code. 53 | 54 | mk_call({atom, _, Name}, Arguments) -> 55 | 'Elixir.Soup.AST.Call':new(Name, Arguments). 56 | 57 | mk_variable({atom, _, Name}) -> 58 | 'Elixir.Soup.AST.Variable':new(Name). 59 | 60 | mk_function(Arguments, Body) -> 61 | 'Elixir.Soup.AST.Function':new(Arguments, Body). 62 | 63 | mk_call_arguments(First, Rest) -> 64 | [First|Rest]. 65 | 66 | mk_fn_arguments({atom, _, Name}, Rest) -> 67 | [Name|Rest]. 68 | 69 | mk_block(Expr) -> 70 | 'Elixir.Soup.AST.Block':new([Expr]). 71 | 72 | mk_block(Expr, Block) -> 73 | #{expressions := Exprs} = Block, 74 | 'Elixir.Soup.AST.Block':new([Expr|Exprs]). 75 | 76 | mk_number({number, _Line, Number}) -> 77 | 'Elixir.Soup.AST.Number':new(Number). 78 | 79 | mk_true({true, _Line}) -> 80 | 'Elixir.Soup.AST.True':new(). 81 | 82 | mk_false({false, _Line}) -> 83 | 'Elixir.Soup.AST.False':new(). 84 | 85 | mk_eq(X, Y) -> 86 | 'Elixir.Soup.AST.Eq':new(X, Y). 87 | 88 | mk_add(X, Y) -> 89 | 'Elixir.Soup.AST.Add':new(X, Y). 90 | 91 | mk_subtract(X, Y) -> 92 | 'Elixir.Soup.AST.Subtract':new(X, Y). 93 | 94 | mk_less_than(X, Y) -> 95 | 'Elixir.Soup.AST.LessThan':new(X, Y). 96 | 97 | mk_if(Pred, X, Y) -> 98 | 'Elixir.Soup.AST.If':new(Pred, X, Y). 99 | 100 | mk_let({atom, _, Name}, Value) -> 101 | 'Elixir.Soup.AST.Let':new(Name, Value). 102 | -------------------------------------------------------------------------------- /src/soup_tokenizer.xrl: -------------------------------------------------------------------------------- 1 | Definitions. 2 | 3 | Int = [0-9]+ 4 | Float = [0-9]+\.[0-9]+ 5 | WS = [\n\s\r\t] 6 | Atom = [a-z_][a-zA-Z0-9!\?_]* 7 | Cmt = \/\/[^\n]* 8 | 9 | Rules. 10 | 11 | if : {token, {'if', TokenLine}}. 12 | let : {token, {'let', TokenLine}}. 13 | else : {token, {else, TokenLine}}. 14 | true : {token, {true, TokenLine}}. 15 | false : {token, {false, TokenLine}}. 16 | \== : {token, {'==', TokenLine}}. 17 | \= : {token, {'=', TokenLine}}. 18 | \+ : {token, {'+', TokenLine}}. 19 | \- : {token, {'-', TokenLine}}. 20 | \< : {token, {'<', TokenLine}}. 21 | \| : {token, {'|', TokenLine}}. 22 | \, : {token, {',', TokenLine}}. 23 | \( : {token, {'(', TokenLine}}. 24 | \) : {token, {')', TokenLine}}. 25 | \{ : {token, {'{', TokenLine}}. 26 | \} : {token, {'}', TokenLine}}. 27 | {Int} : {token, {number, TokenLine, int(TokenChars)}}. 28 | {Float} : {token, {number, TokenLine, flt(TokenChars)}}. 29 | {Atom} : {token, {atom, TokenLine, list_to_atom(TokenChars)}}. 30 | {Cmt} : skip_token. 31 | {WS} : skip_token. 32 | 33 | 34 | Erlang code. 35 | 36 | int(S) when is_list(S) -> 37 | {I, _} = string:to_integer(S), 38 | I. 39 | 40 | flt(S) when is_list(S) -> 41 | {F, _} = string:to_float(S), 42 | F. 43 | -------------------------------------------------------------------------------- /test/soup/ast/add_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.AddTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Add 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.{Add, Number} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduction of add of numbers" do 12 | expr = Add.new(Number.new(1), Number.new(2)) 13 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 14 | assert expr == Number.new(3) 15 | end 16 | 17 | test "reduction of nested lhs" do 18 | expr = Add.new( 19 | Add.new(Number.new(1), Number.new(2)), 20 | Number.new(3)) 21 | 22 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 23 | assert expr == Add.new(Number.new(3), Number.new(3)) 24 | 25 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 26 | assert expr == Number.new(6) 27 | end 28 | 29 | test "reduction of nested both side" do 30 | expr = Add.new( 31 | Add.new(Number.new(1), Number.new(2)), 32 | Add.new(Number.new(3), Number.new(4))) 33 | 34 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 35 | assert expr == Add.new( 36 | Number.new(3), 37 | Add.new(Number.new(3), Number.new(4))) 38 | 39 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 40 | assert expr == Add.new(Number.new(3), Number.new(7)) 41 | 42 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 43 | assert expr == Number.new(10) 44 | end 45 | end 46 | 47 | describe "AST.to_source/2" do 48 | test "Add printing" do 49 | num = Add.new(Number.new(13), Number.new(0)) 50 | assert AST.to_source(num) == "13 + 0" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/soup/ast/block_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.AST.BlockTest do 2 | use ExUnit.Case, async: true 3 | doctest Soup.AST.Block 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.{Block, Add, Number} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/1" do 11 | test "the first expression is discarded if not reducible" do 12 | block = Block.new([Number.new(1), Number.new(2)]) 13 | assert {:ok, next, @env} = AST.reduce(block, @env) 14 | assert next == Block.new([Number.new(2)]) 15 | end 16 | 17 | test "the first expression is reduced if reducible" do 18 | block = Block.new([Add.new(Number.new(1), Number.new(2)), 19 | Number.new(4)]) 20 | assert {:ok, next, @env} = AST.reduce(block, @env) 21 | assert next == Block.new([Number.new(3), Number.new(4)]) 22 | end 23 | 24 | test "reduces to last expression if it is not reducible" do 25 | block = Block.new([Number.new(1)]) 26 | assert {:ok, next, @env} = AST.reduce(block, @env) 27 | assert next == Number.new(1) 28 | end 29 | end 30 | 31 | 32 | describe "AST.to_source/1" do 33 | test "numbers printing" do 34 | num = Block.new([Number.new(64), Number.new(128)]) 35 | assert AST.to_source(num) == """ 36 | 64 37 | 128 38 | """ 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/soup/ast/call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.CallTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Call 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{Call, Return, Add, Number, Function, Variable} 7 | 8 | @env Env.new 9 | 10 | describe "AST.reduce/2" do 11 | test "it throws on unset function" do 12 | env = Env.new 13 | ast = Call.new(:size, []) 14 | thrown = catch_throw(AST.reduce(ast, env)) 15 | assert thrown == {:undefined_function, :size} 16 | end 17 | 18 | test "reduces arguments" do 19 | ast = Call.new(:width, [Add.new(Number.new(1), Number.new(2))]) 20 | assert {:ok, res, @env} = AST.reduce(ast, @env) 21 | assert res == Call.new(:width, [Number.new(3)]) 22 | end 23 | 24 | test "reduces multiple arguments" do 25 | ast = Call.new(:width, [Add.new(Number.new(1), Number.new(2)), 26 | Add.new(Number.new(5), Number.new(2))]) 27 | assert {:ok, res1, @env} = AST.reduce(ast, @env) 28 | assert res1 == Call.new(:width, [Number.new(3), 29 | Add.new(Number.new(5), Number.new(2))]) 30 | assert {:ok, res2, @env} = AST.reduce(res1, @env) 31 | assert res2 == Call.new(:width, [Number.new(3), Number.new(7)]) 32 | end 33 | 34 | test "it reduces to the body with new scope with args set" do 35 | identity = Function.new([:x], Variable.new(:x)) 36 | env = @env |> Env.put(:identity, identity) |> Env.put(:y, Number.new(50)) 37 | call = Call.new(:identity, [Number.new(1)]) 38 | assert {:ok, expanded, new_env} = AST.reduce(call, env) 39 | # The AST is that of the function body, wrapped in a Return 40 | assert expanded == Return.new(Variable.new(:x)) # TODO 41 | # A new scope is created with function and args set 42 | assert Env.get(new_env, :y) == :not_set 43 | assert Env.get(new_env, :x) == {:ok, Number.new(1)} 44 | assert Env.get(new_env, :identity) == {:ok, identity} 45 | end 46 | 47 | test "it throws when too many args" do 48 | identity = Function.new([:x], Variable.new(:x)) 49 | env = @env |> Env.put(:identity, identity) 50 | call = Call.new(:identity, [Number.new(1), Number.new(2)]) 51 | thrown = catch_throw(AST.reduce(call, env)) 52 | assert thrown == {:invalid_arity, :identity} 53 | end 54 | 55 | test "it throws when too few args" do 56 | identity = Function.new([:x], Variable.new(:x)) 57 | env = @env |> Env.put(:identity, identity) 58 | call = Call.new(:identity, []) 59 | thrown = catch_throw(AST.reduce(call, env)) 60 | assert thrown == {:invalid_arity, :identity} 61 | end 62 | end 63 | 64 | describe "AST.to_source" do 65 | test "Call printing" do 66 | call = Call.new(:print, [Number.new(2)]) 67 | assert AST.to_source(call) == "print(2)" 68 | end 69 | 70 | test "multi-arity call printing" do 71 | call = Call.new(:merge, [Number.new(1), Number.new(2), Number.new(3)]) 72 | assert AST.to_source(call) == "merge(1, 2, 3)" 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/soup/ast/eq_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.EqTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Eq 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.{Eq, Add, Number, True, False} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduction of Eq unequal" do 12 | expr = Eq.new(Number.new(1), Number.new(2)) 13 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 14 | assert expr == False.new 15 | end 16 | 17 | test "reduction of Eq equal" do 18 | expr = Eq.new(Number.new(2), Number.new(2)) 19 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 20 | assert expr == True.new 21 | end 22 | 23 | test "reduction of nested lhs" do 24 | expr = Eq.new( 25 | Add.new(Number.new(1), Number.new(2)), 26 | Number.new(3)) 27 | 28 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 29 | assert expr == Eq.new(Number.new(3), Number.new(3)) 30 | 31 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 32 | assert expr == True.new 33 | end 34 | 35 | test "reduction of nested both side" do 36 | expr = Eq.new( 37 | Add.new(Number.new(1), Number.new(2)), 38 | Add.new(Number.new(3), Number.new(4))) 39 | 40 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 41 | assert expr == Eq.new( 42 | Number.new(3), 43 | Add.new(Number.new(3), Number.new(4))) 44 | 45 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 46 | assert expr == Eq.new(Number.new(3), Number.new(7)) 47 | 48 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 49 | assert expr == False.new 50 | end 51 | end 52 | 53 | describe "AST.to_source/2" do 54 | test "Eq printing" do 55 | num = Eq.new(Number.new(13), Number.new(0)) 56 | assert AST.to_source(num) == "13 == 0" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/soup/ast/false_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.FalseTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.False 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.False 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduction of add of numbers" do 12 | expr = False.new() 13 | assert AST.reduce(expr, @env) == :noop 14 | end 15 | end 16 | 17 | describe "AST.to_source/2" do 18 | test "Add printing" do 19 | num = False.new() 20 | assert AST.to_source(num) == "false" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/soup/ast/function_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.FunctionTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Function 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{Function, Variable} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "condition is reduced" do 12 | identity = Function.new([:a], Variable.new(:a)) 13 | assert :noop == AST.reduce(identity, @env) 14 | end 15 | end 16 | 17 | describe "AST.to_source" do 18 | test "Function printing" do 19 | identity = Function.new([:a], Variable.new(:a)) 20 | assert AST.to_source(identity) == """ 21 | |a| { 22 | a 23 | } 24 | """ 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/soup/ast/if_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.IfTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.If 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{If, Add, True, False, Number} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "return consequence if condition is true" do 12 | expr = If.new(True.new(), Number.new(1), Number.new(2)) 13 | assert {:ok, res, @env} = AST.reduce(expr, @env) 14 | assert Number.new(1) == res 15 | end 16 | 17 | test "return alternative if condition is false" do 18 | expr = If.new(False.new(), Number.new(1), Number.new(2)) 19 | assert {:ok, res, @env} = AST.reduce(expr, @env) 20 | assert res == Number.new(2) 21 | end 22 | 23 | test "return alternative if condition is other non-false" do 24 | expr = If.new(Number.new(50), Number.new(10), Number.new(20)) 25 | assert {:ok, res, @env} = AST.reduce(expr, @env) 26 | assert res == Number.new(10) 27 | end 28 | 29 | test "condition is reduced" do 30 | condition = Add.new(Number.new(5), Number.new(5)) 31 | expr = If.new(condition, Number.new(10), Number.new(20)) 32 | assert {:ok, res, @env} = AST.reduce(expr, @env) 33 | assert res == If.new(Number.new(10), Number.new(10), Number.new(20)) 34 | end 35 | end 36 | 37 | describe "AST.to_source" do 38 | test "If printing" do 39 | num = If.new(True.new(), Number.new(1), Number.new(2)) 40 | assert AST.to_source(num) == """ 41 | if true { 42 | 1 43 | } else { 44 | 2 45 | } 46 | """ 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/soup/ast/less_than_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.LessThanTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.LessThan 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{LessThan, True, False, Add, Number} 7 | 8 | @env Env.new() 9 | 10 | describe "Soup.AST.reduce" do 11 | test "numbers can be less than" do 12 | expr = LessThan.new(Number.new(1), Number.new(2)) 13 | assert {:ok, res, @env} = AST.reduce(expr, @env) 14 | assert True.new() == res 15 | end 16 | 17 | test "numbers can be equal" do 18 | expr = LessThan.new(Number.new(1), Number.new(1)) 19 | assert {:ok, res, @env} = AST.reduce(expr, @env) 20 | assert False.new() == res 21 | end 22 | 23 | test "numbers can be more than" do 24 | expr = LessThan.new(Number.new(2), Number.new(1)) 25 | assert {:ok, res, @env} = AST.reduce(expr, @env) 26 | assert False.new() == res 27 | end 28 | 29 | test "lhs is reduced" do 30 | expr = LessThan.new(Add.new(Number.new(1), Number.new(1)), Number.new(1)) 31 | assert {:ok, res, @env} = AST.reduce(expr, @env) 32 | assert LessThan.new(Number.new(2), Number.new(1)) == res 33 | end 34 | 35 | # test "rhs is reduced" do 36 | # expr = LessThan.new(Number.new(1), Add.new(Number.new(1), Number.new(1))) 37 | # assert {:ok, res} = AST.reduce(expr, @env) 38 | # assert LessThan.new(Number.new(2), Number.new(1)) == res 39 | # end 40 | end 41 | 42 | 43 | describe "Soup.AST.to_source" do 44 | test "If printing" do 45 | expr = LessThan.new(Number.new(1), Number.new(2)) 46 | assert AST.to_source(expr) == "1 < 2" 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/soup/ast/let_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.LetTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Let 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{Let, Number, Add} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduces to the value" do 12 | ast = Let.new(:foo, Number.new(1)) 13 | assert {:ok, res, _env} = AST.reduce(ast, @env) 14 | assert res == Number.new(1) 15 | end 16 | 17 | test "it sets the value in the environment" do 18 | ast = Let.new(:foo, Number.new(1)) 19 | assert {:ok, _res, env} = AST.reduce(ast, @env) 20 | assert Env.get(env, :foo) == {:ok, Number.new(1)} 21 | end 22 | 23 | test "it reduces the value" do 24 | ast = Let.new(:thingy, Add.new(Number.new(1), Number.new(2))) 25 | assert {:ok, new_ast, @env} = AST.reduce(ast, @env) 26 | assert new_ast == Let.new(:thingy, Number.new(3)) 27 | end 28 | end 29 | 30 | describe "AST.to_source" do 31 | test "Let printing" do 32 | let = Let.new(:x, Number.new(5)) 33 | assert AST.to_source(let) == "let x = 5" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/soup/ast/number_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.NumberTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Number 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.Number 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/1" do 11 | test "numbers are not reducible" do 12 | num = Number.new(64) 13 | assert AST.reduce(num, @env) == :noop 14 | end 15 | end 16 | 17 | describe "AST.to_source/1" do 18 | test "numbers printing" do 19 | num = Number.new(64) 20 | assert AST.to_source(num) == "64" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/soup/ast/return_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.ReturnTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Return 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.{Return, Number, Add} 7 | 8 | @env Env.new 9 | 10 | describe "AST.reduce/1" do 11 | test "it reduces the body" do 12 | return = Return.new(Add.new(Number.new(10), Number.new(10))) 13 | assert {:ok, result, @env} = AST.reduce(return, @env) 14 | assert Return.new(Number.new(20)) == result 15 | end 16 | 17 | test "it pops the stack as it reduces" do 18 | env = Env.new |> Env.push_stack() 19 | return = Return.new(Number.new(10)) 20 | assert Env.stack_size(env) == 2 21 | assert {:ok, result, new_env} = AST.reduce(return, env) 22 | assert Number.new(10) == result 23 | assert Env.stack_size(new_env) == 1 24 | end 25 | end 26 | 27 | describe "AST.to_source/1" do 28 | test "returns are invisible!" do 29 | num = Return.new(Number.new(64)) 30 | assert AST.to_source(num) == "64" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/soup/ast/subtract_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.SubtractTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Subtract 4 | 5 | alias Soup.{AST, Env} 6 | alias Soup.AST.{Subtract, Number} 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduction of add of numbers" do 12 | expr = Subtract.new(Number.new(5), Number.new(2)) 13 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 14 | assert expr == Number.new(3) 15 | end 16 | 17 | test "reduction of nested lhs" do 18 | expr = Subtract.new( 19 | Subtract.new(Number.new(5), Number.new(2)), 20 | Number.new(1)) 21 | 22 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 23 | assert expr == Subtract.new(Number.new(3), Number.new(1)) 24 | 25 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 26 | assert expr == Number.new(2) 27 | end 28 | 29 | test "reduction of nested both side" do 30 | expr = Subtract.new( 31 | Subtract.new(Number.new(1), Number.new(2)), 32 | Subtract.new(Number.new(3), Number.new(4))) 33 | 34 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 35 | assert expr == Subtract.new( 36 | Number.new(-1), 37 | Subtract.new(Number.new(3), Number.new(4))) 38 | 39 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 40 | assert expr == Subtract.new(Number.new(-1), Number.new(-1)) 41 | 42 | assert {:ok, expr, @env} = AST.reduce(expr, @env) 43 | assert expr == Number.new(0) 44 | end 45 | end 46 | 47 | describe "AST.to_source/2" do 48 | test "Subtract printing" do 49 | num = Subtract.new(Number.new(13), Number.new(10)) 50 | assert AST.to_source(num) == "13 - 10" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/soup/ast/true_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.TrueTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.True 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.True 7 | 8 | @env Env.new() 9 | 10 | describe "AST.reduce/2" do 11 | test "reduction of add of numbers" do 12 | expr = True.new() 13 | assert AST.reduce(expr, @env) == :noop 14 | end 15 | end 16 | 17 | describe "AST.to_source/2" do 18 | test "Add printing" do 19 | bool = True.new() 20 | assert AST.to_source(bool) == "true" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/soup/ast/variable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.VariableTest do 2 | use ExUnit.Case 3 | doctest Soup.AST.Variable 4 | 5 | alias Soup.{Env, AST} 6 | alias Soup.AST.{Variable, Number} 7 | 8 | describe "AST.reduce/2" do 9 | test "reduces to the value under the name in the env" do 10 | env = Env.new |> Env.put(:size, Number.new(2)) 11 | ast = Variable.new(:size) 12 | assert {:ok, res, ^env} = AST.reduce(ast, env) 13 | assert res == Number.new(2) 14 | end 15 | 16 | test "it throws on unset variable" do 17 | env = Env.new 18 | ast = Variable.new(:size) 19 | thrown = catch_throw(AST.reduce(ast, env)) 20 | assert thrown == {:undefined_variable, :size} 21 | end 22 | end 23 | 24 | describe "AST.to_source" do 25 | test "Variable printing" do 26 | var = Variable.new(:x) 27 | assert AST.to_source(var) == "x" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/soup/env_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.EnvTest do 2 | use ExUnit.Case, async: true 3 | doctest Soup.Env 4 | 5 | alias Soup.Env 6 | alias Soup.AST.Number 7 | 8 | describe "Env.put and Env.get" do 9 | test "putting and getting in the current scope" do 10 | env = Env.new 11 | assert :not_set == Env.get(env, :x) 12 | new_env = Env.put(env, :x, Number.new(4)) 13 | assert {:ok, Number.new(4)} == Env.get(new_env, :x) 14 | end 15 | end 16 | 17 | describe "push_stack/1 and pop_stack/1" do 18 | test "grows and shrinks the stack with clean scopes" do 19 | env = 20 | Env.new 21 | |> Env.put(:x, Number.new(4)) 22 | |> Env.push_stack() 23 | assert :not_set == Env.get(env, :x) 24 | new_env = Env.pop_stack(env) 25 | assert {:ok, Number.new(4)} == Env.get(new_env, :x) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/soup/machine_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.MachineTest do 2 | use ExUnit.Case, async: true 3 | doctest Soup.Machine 4 | 5 | alias Soup.{Machine, Env} 6 | alias Soup.AST.{Number, Add} 7 | 8 | describe "new/1" do 9 | test "machine construction" do 10 | machine = Machine.new(Number.new(5)) 11 | assert machine.env == Env.new 12 | assert machine.ast == Number.new(5) 13 | end 14 | end 15 | 16 | describe "step/1" do 17 | test "AST is reduced once" do 18 | machine = Machine.new(Add.new(Number.new(5), Number.new(1))) 19 | assert {:ok, stepped} = Machine.step(machine) 20 | assert stepped.env == Env.new 21 | assert stepped.ast == Number.new(6) 22 | end 23 | 24 | test ":noop when AST cannot be reduced" do 25 | machine = Machine.new(Number.new(5)) 26 | assert :noop == Machine.step(machine) 27 | end 28 | end 29 | 30 | describe "run/1" do 31 | test "AST is fully reduced" do 32 | add1 = fn(x) -> Add.new(x, Number.new(1)) end 33 | ast = add1.(add1.(add1.(add1.(add1.(Number.new(1)))))) 34 | assert {:ok, machine} = ast |> Machine.new() |> Machine.run() 35 | assert machine.env == Env.new() 36 | assert machine.ast == Number.new(6) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/soup/source_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Soup.SourceTest do 2 | use ExUnit.Case, async: true 3 | doctest Soup.Source 4 | 5 | alias Soup.Source 6 | alias Soup.AST.{Block, Number, True, False, Add, Subtract, LessThan, If, 7 | Let, Variable, Function, Call, Eq} 8 | import Source, only: [tokenize!: 1, parse!: 1] 9 | 10 | describe "tokenize/1" do 11 | test "number tokenization" do 12 | assert [{:number, _, 1}] = tokenize!("1") 13 | assert [{:number, _, 1.1}] = tokenize!("1.1") 14 | assert [{:number, _, 29.12}] = tokenize!("29.12") 15 | assert [{:number, _, 7}] = tokenize!("0007") 16 | end 17 | 18 | test "atom (identifier) tokenization" do 19 | assert [{:atom, _, :hi}] = tokenize!("\n\nhi") 20 | assert [{:atom, _, :ok?}] = tokenize!("ok?") 21 | assert [{:atom, _, :do_exec!}] = tokenize!("do_exec!") 22 | end 23 | 24 | test "parens tokenization" do 25 | assert [{:"(", _}] = tokenize!("(") 26 | assert [{:")", _}] = tokenize!(")") 27 | assert [{:"(", _}, {:number, _, 1}, {:")", _}] = tokenize!("(1)") 28 | end 29 | 30 | test "brace tokenization" do 31 | assert [{:"{", _}] = tokenize!("{") 32 | assert [{:"}", _}] = tokenize!("}") 33 | assert [{:"{", _}, {:number, _, 1}, {:"}", _}] = tokenize!("{1}") 34 | end 35 | 36 | test "`true` tokenization" do 37 | assert [{:true, _}] = tokenize!("true") 38 | end 39 | 40 | test "`false` tokenization" do 41 | assert [{:false, _}] = tokenize!("false") 42 | end 43 | 44 | test "+ tokenization" do 45 | assert [{:+, _}] = tokenize!("+") 46 | end 47 | 48 | test "- tokenization" do 49 | assert [{:-, _}] = tokenize!("-") 50 | end 51 | 52 | test "< tokenization" do 53 | assert [{:<, _}] = tokenize!("<") 54 | end 55 | 56 | test "== tokenization" do 57 | assert [{:==, _}] = tokenize!("==") 58 | end 59 | 60 | test "= tokenization" do 61 | assert [{:=, _}] = tokenize!("=") 62 | end 63 | 64 | test "| tokenization" do 65 | assert [{:|, _}] = tokenize!("|") 66 | end 67 | 68 | test ", tokenization" do 69 | assert [{:",", _}] = tokenize!(",") 70 | end 71 | 72 | test "if else tokenization" do 73 | assert [{:if, _}, {:"(", _}, {:true, _}, {:")", _}, 74 | {:"{", _}, {:number, _, 1}, {:"}", _}, 75 | {:else, _}, 76 | {:"{", _}, {:number, _, 2}, {:"}", _}, 77 | ] = tokenize!("if (true) { 1 } else { 2 }") 78 | end 79 | 80 | test "let tokenization" do 81 | assert [{:let, _}] = tokenize!("let") 82 | end 83 | 84 | test "comment tokenization" do 85 | assert [{:let, _}] = tokenize!("let // 1 2 3") 86 | assert [{:let, _}] = tokenize!("let//") 87 | end 88 | end 89 | 90 | 91 | describe "parse/1" do 92 | test "number parsing" do 93 | assert parse!("1") == Block.new([Number.new(1)]) 94 | assert parse!("007") == Block.new([Number.new(7)]) 95 | assert parse!("38.44") == Block.new([Number.new(38.44)]) 96 | end 97 | 98 | test "`true` parsing" do 99 | assert parse!("true") == Block.new([True.new()]) 100 | end 101 | 102 | test "`false` parsing" do 103 | assert parse!("false") == Block.new([False.new()]) 104 | end 105 | 106 | test "+ parsing" do 107 | assert parse!("1 + 2") == Block.new([Add.new(Number.new(1), 108 | Number.new(2))]) 109 | assert parse!("false + 6") == Block.new([Add.new(False.new(), 110 | Number.new(6))]) 111 | end 112 | 113 | test "- parsing" do 114 | assert parse!("8 - 3") == Block.new([Subtract.new(Number.new(8), 115 | Number.new(3))]) 116 | end 117 | 118 | test "== parsing" do 119 | assert parse!("1 == 2") == Block.new([Eq.new(Number.new(1), 120 | Number.new(2))]) 121 | end 122 | 123 | test "< parsing" do 124 | assert parse!("1 < 2") == Block.new([LessThan.new(Number.new(1), 125 | Number.new(2))]) 126 | assert parse!("1 + 1 < 2") == 127 | Block.new([LessThan.new(Add.new(Number.new(1), Number.new(1)), 128 | Number.new(2))]) 129 | end 130 | 131 | test "if parsing" do 132 | assert parse!("if true { 1 } else { 2 }") == 133 | Block.new([If.new(True.new, 134 | Block.new([Number.new(1)]), 135 | Block.new([Number.new(2)]))]) 136 | end 137 | 138 | test "assignment" do 139 | assert parse!("let x = 10") == Block.new([Let.new(:x, Number.new(10))]) 140 | end 141 | 142 | test "block parsing" do 143 | assert parse!(""" 144 | let a = 10 145 | let b = 20 146 | let c = 30 147 | """) == Block.new([Let.new(:a, Number.new(10)), 148 | Let.new(:b, Number.new(20)), 149 | Let.new(:c, Number.new(30))]) 150 | end 151 | 152 | test "function parsing" do 153 | assert parse!("|a| { a }") == 154 | Block.new([Function.new([:a], 155 | Block.new([Variable.new(:a)]))]) 156 | assert parse!(""" 157 | |a, b| { 158 | a + b 159 | } 160 | """) == 161 | Block.new([Function.new([:a, :b], 162 | Block.new([Add.new(Variable.new(:a), 163 | Variable.new(:b))]))]) 164 | end 165 | 166 | test "call parsing" do 167 | assert parse!("print(1)") == 168 | Block.new([Call.new(:print, [Number.new(1)])]) 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /test/soup_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SoupTest do 2 | use ExUnit.Case 3 | 4 | examples = 5 | [Application.app_dir(:soup), "priv", "code", "*.soup"] 6 | |> Path.join() 7 | |> Path.wildcard() 8 | 9 | for path <- examples do 10 | name = Path.basename(path) 11 | code = File.read!(path) 12 | 13 | @tag :integration 14 | test "eval-ing `priv/code/#{name}`" do 15 | assert Soup.eval(unquote(code)) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------