├── .codecov.yml ├── .gitignore ├── .travis.yml ├── README.md ├── ast.go ├── ast_test.go ├── backend.go ├── backend └── example-scores-php │ ├── bento.json │ ├── scores.bento │ └── scores.php ├── compiler.go ├── compiler_test.go ├── examples ├── am-pm.bento ├── custom-sentences.bento ├── hello-world.bento └── variables.bento ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── number.go ├── number_test.go ├── parser.go ├── parser_test.go ├── system.go ├── tests ├── add.bento ├── add.txt ├── blackhole.bento ├── blackhole.txt ├── display.bento ├── display.txt ├── divide.bento ├── divide.txt ├── functions.bento ├── functions.txt ├── if.bento ├── if.txt ├── multiline.bento ├── multiline.txt ├── multiply.bento ├── multiply.txt ├── number.bento ├── number.txt ├── question.bento ├── question.txt ├── subtract.bento ├── subtract.txt ├── system.bento ├── system.darwin.txt ├── system.linux.txt ├── text.bento ├── text.txt ├── unless.bento ├── unless.txt ├── until.bento ├── until.txt ├── while.bento └── while.txt ├── token.go ├── token_test.go ├── variables.go ├── vm.go └── vm_test.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: no 4 | patch: no 5 | changes: no 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /bento 3 | /gh-md-toc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: "1.12.x" 4 | 5 | env: 6 | global: 7 | # This is only needed until Go 1.13 when it will be the default. 8 | - GO111MODULE=on 9 | 10 | stages: 11 | - Test 12 | - name: Release 13 | if: tag =~ ^v 14 | 15 | jobs: 16 | include: 17 | - stage: Test 18 | script: 19 | - $(exit $(go fmt ./... | wc -l)) 20 | - go test ./... -race -coverprofile=coverage.txt -covermode=atomic 21 | 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | 25 | - stage: Release 26 | script: 27 | - GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bento && zip bento-linux.zip bento 28 | - GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o bento && zip bento-mac.zip bento 29 | - GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bento.exe && zip bento-windows.zip bento.exe 30 | 31 | deploy: 32 | provider: releases 33 | api_key: 34 | secure: "TIQmDVN5PqMlSAk5pEb0pOhoUEMRkHRcTtdOATxFMqwL2GG70nzsU/e3jzXxK7KEo+sSRTXUW7tlLZVZaopChPPvGYKj1rSaamdLaS8IcLQfafsP0A7dmLSpXdiHEHxXzZsqsd3rzNZIjCdxv7iFbe6wU9nivB6wpTqlt8kdGiO6qlm33VyfVgF3GBV/4U4C/y8Ef0OAnX14+a2zii4Enq0nKWFuvQ3p+2DXU/KZShZnnplOokCwigyyPL2/jltoewUQDsDuNi5V2ugR3E3iixCIM5epPsWNJ8uK3DVO1ZpWpQt/n0IDirK2ElTMUcNwio0X/SF3C6GdI1VgAUzBuVnjrHbDy/7lfmM1R1+1SzLSn6vhd8/OIMXsp9eAgAI9WLxTgPL6O1cZbQUN6EduapR8tMmx6PQuFf10CjhaRQIuIGYBtsbT0aoaMdj4jLUHBn4Y5968tS2tv9LLP2bcleDJd+We6kRqY5P48eD/9dwa88CfVKCO0SBQbiAkiD23jDf2boPfbic4wBbxd04tGbboQsPr/XOlLa2nIh6PY1bxkNNdBfQM1guWDzdjM2nseUPQIe672QUc4X0gKEMzbOUF+zlEA1caxBiWIcW2PiDNTqDDBqgwIbZnnFbXwUcf3SrAemsUCCjjnNVi9Jk/qBXAgO8FjAKMo/jNJ8nesCM=" 35 | skip_cleanup: true 36 | file: 37 | - bento-linux.zip 38 | - bento-mac.zip 39 | - bento-windows.zip 40 | on: 41 | repo: elliotchance/bento 42 | all_branches: true 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍱 bento 2 | 3 | * [Getting Started](#getting-started) 4 | * [Installation](#installation) 5 | * [Running A Program](#running-a-program) 6 | * [Example Use Case](#example-use-case) 7 | * [Language](#language) 8 | * [File Structure](#file-structure) 9 | * [Sentences](#sentences) 10 | * [Wrapping Long Sentences](#wrapping-long-sentences) 11 | * [Comments](#comments) 12 | * [Variables](#variables) 13 | * [Blackhole](#blackhole) 14 | * [Text](#text) 15 | * [Number](#number) 16 | * [Mathematical Operations](#mathematical-operations) 17 | * [Functions](#functions) 18 | * [Arguments](#arguments) 19 | * [Questions](#questions) 20 | * [Controlling Flow](#controlling-flow) 21 | * [Conditions](#conditions) 22 | * [Decisions (if/unless)](#decisions-ifunless) 23 | * [Loops (while/until)](#loops-whileuntil) 24 | * [Backends](#backends) 25 | * [Locating and Starting Backends](#locating-and-starting-backends) 26 | * [Communication Protocol](#communication-protocol) 27 | * [Request](#request) 28 | * [Response](#response) 29 | * [Special Cases](#special-cases) 30 | * [sentences](#sentences-1) 31 | * [Examples](#examples) 32 | * [PHP](#php) 33 | * [System](#system) 34 | * [Examples](#examples-1) 35 | * [Hello, World!](#hello-world) 36 | * [Variables](#variables-1) 37 | * [Functions (Custom Sentences)](#functions-custom-sentences) 38 | 39 | bento is a 40 | [forth-generation programming language](https://en.wikipedia.org/wiki/Fourth-generation_programming_language) 41 | that is English-based. It is designed to separate orchestration from 42 | implementation as to provide a generic, self-documenting DSL that can be managed 43 | by non-technical individuals. 44 | 45 | That's a bunch of fancy talk that means that the developers are able to 46 | setup the complex tasks and make them easily rerunnable by non-technical people. 47 | 48 | The project is still very young, but it has a primary goal for each half of its 49 | intended audience: 50 | 51 | **For the developer:** Programs can be written in any language and easily 52 | exposed through a set of specific DSLs called "sentences". 53 | 54 | **For the user:** An English-based language that avoids any nuances that 55 | non-technical people would find difficult to understand. The language only 56 | contains a handful of special words required for control flow. Whitespace and 57 | capitalization do not matter. 58 | 59 | # Getting Started 60 | 61 | ## Installation 62 | 63 | Bento is available for Mac, Windows and Linux. You can download the latest 64 | release from the [Releases](https://github.com/elliotchance/bento/releases) 65 | page. 66 | 67 | ## Running A Program 68 | 69 | Create a file called `hello-world.bento` with the contents: 70 | 71 | ```bento 72 | start: 73 | display "Hello, World!" 74 | ``` 75 | 76 | Then run this file with (you may need to replace the path to the file if you are 77 | not in the same directory as the file): 78 | 79 | ```bash 80 | bento hello-world.bento 81 | ``` 82 | 83 | # Example Use Case 84 | 85 | The sales team need to be able to run customer reports against a database. They 86 | do not understand SQL, or the reports may need to be generated from multiple 87 | tools/steps. As the developer, your primary options are: 88 | 89 | 1. Run the reports for them. This is a waste of engineering time and a blocker 90 | for the sales team if they need to do it frequently, or while you're on holiday. 91 | 92 | 2. Write up some documentation and give them readonly access to what they need. 93 | The process could be highly technical. Also, allowing them to access production 94 | systems is a bad idea. Also, it's still a waste of time for everyone when things 95 | aren't working as expected. 96 | 97 | 3. Build a tool that runs the reports for them. This is another rabbit hole of 98 | wasted engineering time, especially when requirements change frequently or they 99 | need a lot of flexibility. 100 | 101 | 4. Build the reporting queries/tasks once in bento, and provide them with a 102 | script that they can edit and rerun. An example might look like: 103 | 104 | ``` 105 | run sales report for last 7 days for customer 123 106 | if there were sales, email report to "bob@mycompany.com" 107 | ``` 108 | 109 | The developer would implement the following sentences: 110 | 111 | ``` 112 | run sales report for last ? days for customer ? 113 | there were sales 114 | email report to ? 115 | ``` 116 | 117 | # Language 118 | 119 | ## File Structure 120 | 121 | A `.bento` file consists of one or more functions. Each of those functions can 122 | contain zero or more sentences and call other sentences before of after they are 123 | defined. 124 | 125 | The file must contain a `start` function. This can be located anywhere in the\ 126 | file, but it is recommended to put it as the first function. 127 | 128 | ## Sentences 129 | 130 | A sentence contains a collection of words and values and it is terminated by a 131 | new line. For example: 132 | 133 | ``` 134 | display "Hello" 135 | ``` 136 | 137 | ### Wrapping Long Sentences 138 | 139 | You can explicitly use `...` at the end of the line to indicate a continuation: 140 | 141 | ```bento 142 | this is a really long... 143 | sentence that should go... 144 | over multiple lines 145 | ``` 146 | 147 | Indentation between lines does not make an difference. However, it is easier to 148 | read when following lines are indented. 149 | 150 | Sentences can also contains new lines if the line ends with a `,`. This is 151 | useful for long inline statements: 152 | 153 | ```bento 154 | if my-name != "John", 155 | display "oops!", 156 | otherwise display "All good." 157 | ``` 158 | 159 | ## Comments 160 | 161 | Comments start with a `#` and continue until a new line or the end of the 162 | program is reached. The comment may be on its own line or at the end of a 163 | sentence: 164 | 165 | ``` 166 | # A comment looks like this. 167 | display "Hello" # It can also be here 168 | ``` 169 | 170 | ## Variables 171 | 172 | ``` 173 | declare first-name is text 174 | declare counter is a number 175 | ``` 176 | 177 | 1. Only `text` and `number` is supported. See specific documentation below. 178 | 2. The word `a` or `an` may appear before the type. This can make it easier to 179 | read: "is a number" rather than "is number". However, the "a" or "an" does not 180 | have any affect on the program. 181 | 3. All variables for a function must be declare before any other sentences. 182 | 4. The same variable name cannot be defined twice within the same function. 183 | However, the same variable name can appear in different functions. These are 184 | unrelated to each other. 185 | 5. You cannot declare a variable with the same name as one of the function 186 | parameters. 187 | 6. All types have a default value which is safe to use before it is set to 188 | another value. 189 | 7. There is a special variable called `_` which is called the blackhole. 190 | Explained in more detail below. 191 | 192 | Variables can be set with: 193 | 194 | ``` 195 | set first-name to "Bob" 196 | ``` 197 | 198 | ### Blackhole 199 | 200 | The blackhole variable is a single underscore (`_`). It can be used as a 201 | placeholder when the value can be ignored. For example: 202 | 203 | ```bento 204 | divide 1.23 by 7.89 into _ 205 | ``` 206 | 207 | If `_` is used in a place where the value would be read it will return a zero 208 | value (the same as the default value for that type). 209 | 210 | The following lines would function the same. However, you should not rely on the 211 | blackhole variable as a readable value and it may be made illegal in the future: 212 | 213 | ```bento 214 | add _ and 7.89 into result 215 | add 0 and 7.89 into result 216 | ``` 217 | 218 | ### Text 219 | 220 | ```bento 221 | my-variable is text 222 | ``` 223 | 224 | 1. A `text` variable can contain any text, including being empty (zero 225 | characters). 226 | 2. It's perfectly safe to use text variables before they have been given a 227 | value, the default value will be empty. 228 | 229 | ### Number 230 | 231 | ```bento 232 | my-variable is number 233 | my-variable is number with 1 decimal place 234 | my-variable is number with 3 decimal places 235 | ``` 236 | 237 | 1. A number variable is exact and has a maximum number of decimal places (this 238 | is also called the precision). 239 | 2. If the number of decimal places is not specified it will use 6. 240 | 3. For integers you should use `number with 0 decimal places`. 241 | 4. The number of decimal places cannot be negative. 242 | 5. A number has no practical minimum (negative) or maximum (positive) value. You 243 | can process incredibly large numbers with absolute precision. 244 | 6. Any calculated value will be rounded at the end of the operation so that it 245 | never contains more precision than what is allowed. For example if the number 246 | has one decimal place, `5.5 * 6.5 * 11` evaluates to `393.8` because 247 | `5.5 * 6.5 = 35.75 => 35.8`, `35.8 * 11 = 393.8`. 248 | 7. Numbers are always displayed without trailing zeroes after the decimal point. 249 | For example, `12.3100` is displayed as `12.31` as long as the number of decimal 250 | places is at least 2. 251 | 8. The words `places` and `place` mean the same thing. However, it is easier to 252 | read when `place` is reserved for when there is only one decimal place. 253 | 9. The default value of a `number` is `0`. This is safe to use use before it has 254 | been set. 255 | 256 | #### Mathematical Operations 257 | 258 | ```bento 259 | add a and b into c # c = a + b 260 | subtract a from b into c # c = b - c 261 | multiply a and b into c # c = a * b 262 | divide a and b into c # c = a / b 263 | ``` 264 | 265 | Note: Be careful with `subtract` as the operands are in the reverse order of the 266 | others. 267 | 268 | ## Functions 269 | 270 | Functions (custom sentences) can be defined by using the `:` character: 271 | 272 | ``` 273 | print everything: 274 | display "Hello" 275 | display "World" 276 | ``` 277 | 278 | The whitespace is not required. However, it is easier to read when content of 279 | functions are indented with spaces or tabs. 280 | 281 | ### Arguments 282 | 283 | Variables can be declared in the function name by specifying their names and 284 | types in `()`, for example: 285 | 286 | ``` 287 | say greeting to persons-name (greeting is text, persons-name is text): 288 | display greeting 289 | display persons-name 290 | ``` 291 | 292 | Can be called with: 293 | 294 | ``` 295 | say "Hi" to "Bob" 296 | ``` 297 | 298 | The order in which the arguments are defined is not important. 299 | 300 | ### Questions 301 | 302 | A question is a special type of function that is defined with a `?` instead of a 303 | `:`: 304 | 305 | ```bento 306 | it is ok? 307 | yes 308 | ``` 309 | 310 | A question is answered with the `yes` or `no` sentences. Once a question is 311 | answered it will return immediately. 312 | 313 | If a question is not explicitly answered by the end, it's assumed to be `no`. 314 | 315 | Questions can be asked in conditionals: 316 | 317 | ```bento 318 | start: 319 | if it is ok, display "All good!" 320 | ``` 321 | 322 | Questions can also take arguments in the same way that functions do: 323 | 324 | ```bento 325 | start: 326 | declare x is number 327 | 328 | set x to 123 329 | if x is over 100, display "It's over 100", otherwise display "Not yet" 330 | 331 | the-number is over threshold (the-number is number, threshold is number)? 332 | if the-number > threshold, yes 333 | ``` 334 | 335 | ## Controlling Flow 336 | 337 | ### Conditions 338 | 339 | A condition is a simple comparison between two variables or values. Some 340 | examples are: 341 | 342 | ``` 343 | name = "Bob" 344 | counter > 10 345 | first-name != last-name 346 | ``` 347 | 348 | All supported operators are: 349 | 350 | - `=` - Equal. 351 | - `!=` - Not equal. 352 | - `>` - Greater than. 353 | - `>=` - Greater than or equal. 354 | - `<` - Less than. 355 | - `<=` - Less than or equal. 356 | 357 | Values can only be compared when they are the same type. For example the 358 | following is not allowed, and will return an error: 359 | 360 | ``` 361 | "123" = 123 362 | ``` 363 | 364 | ### Decisions (if/unless) 365 | 366 | Sentences starting with `if` or `unless` can be used to control the flow. The 367 | sentence takes one of the following forms (each either starting with `if` or 368 | `unless`): 369 | 370 | ``` 371 | if/unless , 372 | 373 | if/unless , , otherwise 374 | ``` 375 | 376 | When `unless` is used instead of `if` the comparison is inverted, so that: 377 | 378 | ``` 379 | if "Bob" = "Bob" # true 380 | unless "Bob" = "Bob" # false 381 | ``` 382 | 383 | ### Loops (while/until) 384 | 385 | Sentences starting with `while` repeat the sentence until while the condition is 386 | true. That is, the loop will only stop once the condition becomes false. 387 | 388 | Conversely, using `until` will repeat the sentence until the condition becomes 389 | true. 390 | 391 | Loops are written in one of the following forms: 392 | 393 | ``` 394 | while/until , 395 | 396 | while/until , , otherwise 397 | ``` 398 | 399 | # Backends 400 | 401 | A backend is program controlled by bento. A backend can be any program (compiled 402 | or interpreted) that implements the bento protocol on the port specified on the 403 | `BENTO_PORT` environment variable. 404 | 405 | ## Locating and Starting Backends 406 | 407 | A backend is started (that is the program is started) when a variable is 408 | declared with a type that represents the backend. For example: 409 | 410 | ```bento 411 | declare my-var is my-backend 412 | ``` 413 | 414 | Will find and start the backend with the name `my-backend`. The process is: 415 | 416 | 1. `$BENTO_BACKEND` works similar to `$PATH` where it may contain zero or more 417 | paths split by a `:`. If `$BENTO_BACKEND` is not defined or is empty then it 418 | will receive a default value of `.` - the current directory. 419 | 420 | 2. For each of the backend paths, in order, it will attempt to find a directory 421 | called `my-backend`. The first one that it finds will be the one used, even if 422 | another directory of the same name exists in other backend paths. 423 | 424 | 3. The `my-backend` directory must contain a file called `bento.json`. This 425 | describes the backend, and also how it is to be executed. A minimal `bento.json` 426 | looks like: 427 | 428 | ```json 429 | { 430 | "run": "php myscript.php" 431 | } 432 | ``` 433 | 434 | The `run` contains the system command that will be executed. The program is 435 | expected to open a socket, listening on the `BENTO_PORT` environment variable. 436 | 437 | The program must remain running until the socket is closed by bento. All 438 | communication is defined in the *Backend Protocol*. 439 | 440 | ## Communication Protocol 441 | 442 | All communication between bento and the backend is done through a socket. The 443 | port will be provided to the backend with the `BENTO_PORT` environment variable. 444 | 445 | Bento will always start the communication with a request and wait for a 446 | response. This synchronous process will continue indefinitely until bento closes 447 | the connection. You may perform final cleanup if need be, then exit the backend 448 | program. 449 | 450 | A request or response will be a JSON object that consists of a single line, then 451 | terminated by a single new line (`\n`). The newline is important because it 452 | signals to the other side that the end of the message has been reached. It's 453 | also important to make sure JSON objects are encoded correctly so that any new 454 | line characters have been escaped. 455 | 456 | ### Request 457 | 458 | A request object is sent from bento to the backend and looks like: 459 | 460 | ```json 461 | { 462 | "sentence": "add ? to ?", 463 | "args": ["57", "example-scores-php"] 464 | } 465 | ``` 466 | 467 | `sentence` is always a non-empty string. `?` is used as placeholders for the 468 | respective order of elements in `args`. `args` will always be an array that will 469 | contain the same number elements as their are placeholders. 470 | 471 | Each of the `args` will be a string (regardless of the internal type in bento). 472 | 473 | ### Response 474 | 475 | Bento will wait for a response after sending a request before proceeding. Like 476 | the request, the response must be a valid JSON object encoded in a single line, 477 | follow by a new line character to signal termination. 478 | 479 | A response can contain the following keys: 480 | 481 | ```json 482 | { 483 | "text": "something", 484 | "set": { 485 | "$0": "foo" 486 | }, 487 | "error": "Oh-noes!" 488 | } 489 | ``` 490 | 491 | - `text` - The text representation of the variable. This is what is output with 492 | `display ?`. You do not need to return a value in other cases. You must 493 | implement the `display ?` sentence in your backend for this feature. 494 | 495 | - `set` - This will set the value of a variable based on it's index in the 496 | sentence (`$n` where `n` is an index). The first placeholder (`?`) will have an 497 | index of `0`. The value must be a string and a valid valid for the destination 498 | type. 499 | 500 | - `error` must exist and be a string when an error has occurred. It also must 501 | not be empty. The `error` should contain a description of the problem in a 502 | human-readable manner. It should not contain sensitive information such as 503 | passwords, or details such as stack traces used for debugging. 504 | 505 | ### Special Cases 506 | 507 | #### sentences 508 | 509 | All backends must implement `sentences`, which is used to fetch all of the 510 | allowed sentences: 511 | 512 | ```json 513 | { 514 | "special": "sentences" 515 | } 516 | ``` 517 | 518 | The response must be in the form: 519 | 520 | ```json 521 | { 522 | "sentences": ["increase ? by ?", "display ?"] 523 | } 524 | ``` 525 | 526 | The `sentences` is allowed to have zero elements. 527 | 528 | The special `sentences` request is sent once, immediately after the socket 529 | connection to the backend is successful. However, you should allow this request 530 | to come at any time and return the same result in all cases. 531 | 532 | ## Examples 533 | 534 | Each of the examples implement the backend for the following: 535 | 536 | ```bento 537 | start: 538 | declare total is my-backend 539 | increase total by 57 540 | increase total by 13 541 | display total 542 | ``` 543 | 544 | The result of running the program in all cases is: 545 | 546 | ``` 547 | The total is 70. 548 | ``` 549 | 550 | ### PHP 551 | 552 | - [backend/example-scores-php](https://github.com/elliotchance/bento/tree/master/backend/example-scores-php). 553 | 554 | ## System 555 | 556 | The system backend provides direct access to running programs on the host 557 | machine. 558 | 559 | - `run system command `: Run the `command` and send all stdout and 560 | stderr to the console. 561 | 562 | - `run system command output into `: Run the `command` and 563 | capture all of the stdout and stderr into the `output`. 564 | 565 | - `run system command status code into `: Run the `command` 566 | and discard and stdout and stderr. Instead capture the status code returned in 567 | `status`. 568 | 569 | - `run system command output into status code into `: 570 | Run the `command` and capture the stdout and stderr into `output` as well as the 571 | status code returned into `status`. 572 | 573 | Example: 574 | 575 | ```bento 576 | start: 577 | declare echo-result is number 578 | run system command "echo hello" status code into echo-result 579 | unless echo-result = 0, display "command failed!" 580 | ``` 581 | 582 | # Examples 583 | 584 | ## Hello, World! 585 | 586 | ``` 587 | start: 588 | display "Hello, World!" 589 | ``` 590 | 591 | ## Variables 592 | 593 | ``` 594 | start: 595 | declare first-name is text 596 | set first-name to "Bob" 597 | display first-name 598 | ``` 599 | 600 | ## Functions (Custom Sentences) 601 | 602 | ``` 603 | start: 604 | print everything 605 | say "Hi" to "Bob" 606 | 607 | print everything: 608 | display "Hello" 609 | display "World" 610 | 611 | say greeting to persons-name (persons-name is text, greeting is text): 612 | display greeting 613 | display persons-name 614 | ``` 615 | -------------------------------------------------------------------------------- /ast.go: -------------------------------------------------------------------------------- 1 | // This file contains structures produced by the parser to describe the syntax 2 | // of the program. 3 | 4 | package main 5 | 6 | import "strings" 7 | 8 | const ( 9 | OperatorEqual = "=" 10 | OperatorNotEqual = "!=" 11 | OperatorGreaterThan = ">" 12 | OperatorGreaterThanEqual = ">=" 13 | OperatorLessThan = "<" 14 | OperatorLessThanEqual = "<=" 15 | ) 16 | 17 | type Statement interface{} 18 | 19 | // Program is the result of root-level AST after parsing the source code. 20 | // 21 | // The program may not be valid. It has to be compiled before it can be 22 | // executed. 23 | type Program struct { 24 | Functions map[string]*Function 25 | } 26 | 27 | func (program *Program) AppendFunction(fn *Function) { 28 | // TODO: Check for duplicate. 29 | program.Functions[fn.Definition.Syntax()] = fn 30 | } 31 | 32 | type Function struct { 33 | Definition *Sentence 34 | 35 | // Variables includes the arguments and locally declared variables. 36 | Variables []*VariableDefinition 37 | 38 | Statements []Statement 39 | 40 | IsQuestion bool 41 | } 42 | 43 | func (fn *Function) VariableMap() map[string]*VariableDefinition { 44 | m := make(map[string]*VariableDefinition) 45 | 46 | for _, variable := range fn.Variables { 47 | m[variable.Name] = variable 48 | } 49 | 50 | return m 51 | } 52 | 53 | func (fn *Function) AppendArgument(name, ty string) { 54 | fn.Variables = append(fn.Variables, &VariableDefinition{ 55 | Name: name, 56 | Type: ty, 57 | LocalScope: false, 58 | }) 59 | } 60 | 61 | func (fn *Function) AppendVariable(definition *VariableDefinition) { 62 | fn.Variables = append(fn.Variables, definition) 63 | } 64 | 65 | func (fn *Function) AppendStatement(statement Statement) { 66 | fn.Statements = append(fn.Statements, statement) 67 | } 68 | 69 | // Sentence is part of the AST. A sentence may not yet exist, or be valid. 70 | type Sentence struct { 71 | Words []interface{} 72 | } 73 | 74 | // Syntax like "add ? to ?" 75 | func (sentence *Sentence) Syntax() string { 76 | var words []string 77 | 78 | for _, word := range sentence.Words { 79 | if s, ok := word.(string); ok { 80 | words = append(words, s) 81 | } else { 82 | words = append(words, "?") 83 | } 84 | } 85 | 86 | return strings.Join(words, " ") 87 | } 88 | 89 | // Each of the values of the placeholders. 90 | func (sentence *Sentence) Args() (args []interface{}) { 91 | for _, word := range sentence.Words { 92 | if _, ok := word.(string); !ok { 93 | args = append(args, word) 94 | } 95 | } 96 | 97 | return 98 | } 99 | 100 | type Condition struct { 101 | Left, Right interface{} 102 | Operator string 103 | } 104 | 105 | type If struct { 106 | // Unless is true if "unless" was used instead of "if". This inverts the 107 | // logic. 108 | Unless bool 109 | 110 | // Either Condition or Question will be not-nil, never both. 111 | Condition *Condition 112 | Question *Sentence 113 | 114 | // The blocks containing the true and false branches. 115 | True, False Statement 116 | } 117 | 118 | type While struct { 119 | // Until is true if "until" was used instead of "while". This inverts the 120 | // logic. 121 | Until bool 122 | 123 | // Either Condition or Question will be not-nil, never both. 124 | Condition *Condition 125 | Question *Sentence 126 | 127 | // The blocks containing the true and false branches. This is a sentence 128 | // because it makes no sense to allow yes/no answers here. 129 | True *Sentence 130 | } 131 | 132 | type QuestionAnswer struct { 133 | Yes bool 134 | } 135 | -------------------------------------------------------------------------------- /ast_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var sentenceTests = map[string]struct { 9 | sentence *Sentence 10 | expectedSyntax string 11 | }{ 12 | "Display": { 13 | sentence: &Sentence{ 14 | Words: []interface{}{ 15 | "display", NewText("hello"), 16 | }, 17 | }, 18 | expectedSyntax: "display ?", 19 | }, 20 | } 21 | 22 | func TestSentence_Syntax(t *testing.T) { 23 | for testName, test := range sentenceTests { 24 | t.Run(testName, func(t *testing.T) { 25 | assert.Equal(t, test.expectedSyntax, test.sentence.Syntax()) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "math/rand" 9 | "net" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type Backend struct { 17 | Name string 18 | Path string // directory of the backend 19 | Port int 20 | Conn net.Conn 21 | Sentences []string 22 | Config *BackendConfiguration 23 | } 24 | 25 | type BackendRequest struct { 26 | Sentence string `json:"sentence"` 27 | Args []string `json:"args"` 28 | } 29 | 30 | type BackendResponse struct { 31 | Text string `json:"text"` 32 | Set map[string]string `json:"set"` 33 | Error string `json:"error"` 34 | } 35 | 36 | type BackendConfiguration struct { 37 | Run string 38 | } 39 | 40 | func NewBackend(name string) *Backend { 41 | return &Backend{ 42 | Name: name, 43 | } 44 | } 45 | 46 | func (backend *Backend) connect() (err error) { 47 | to := fmt.Sprintf("127.0.0.1:%d", backend.Port) 48 | backend.Conn, err = net.Dial("tcp", to) 49 | 50 | return 51 | } 52 | 53 | func (backend *Backend) sendRaw(body string) (string, error) { 54 | _, err := fmt.Fprintln(backend.Conn, body) 55 | if err != nil { 56 | fmt.Printf("%s ! %v\n", backend.Name, err) 57 | return "", err 58 | } 59 | 60 | response, err := bufio.NewReader(backend.Conn).ReadString('\n') 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | return response, err 66 | } 67 | 68 | func (backend *Backend) send(request *BackendRequest) (*BackendResponse, error) { 69 | jsonData, err := json.Marshal(request) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | responseData, err := backend.sendRaw(string(jsonData)) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | var response *BackendResponse 80 | err = json.Unmarshal([]byte(responseData), &response) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return response, nil 86 | } 87 | 88 | func (backend *Backend) loadSentences() error { 89 | response, err := backend.sendRaw(`{"special":"sentences"}`) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | var parsedResponse struct { 95 | Sentences []string 96 | } 97 | err = json.Unmarshal([]byte(response), &parsedResponse) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | backend.Sentences = parsedResponse.Sentences 103 | 104 | return nil 105 | } 106 | 107 | func (backend *Backend) backendDirectories() []string { 108 | directories := os.Getenv("BENTO_BACKEND") 109 | if directories == "" { 110 | // Default to the current directory if none are provided. 111 | directories = "." 112 | } 113 | 114 | return strings.Split(directories, ":") 115 | } 116 | 117 | func (backend *Backend) findBackend() error { 118 | // We always use the first backend that matches the name. Even if there are 119 | // other backends by the same name that would have otherwise been discovered 120 | // in the future. 121 | dirs := backend.backendDirectories() 122 | for _, dir := range dirs { 123 | path := fmt.Sprintf("%s/%s", dir, backend.Name) 124 | fs, err := os.Stat(path) 125 | if err == nil && fs.IsDir() { 126 | backend.Path = path 127 | return nil 128 | } 129 | } 130 | 131 | return fmt.Errorf("no such backend %s in any path: %s", 132 | backend.Name, strings.Join(dirs, ":")) 133 | } 134 | 135 | func (backend *Backend) readBentoConfig() error { 136 | data, err := ioutil.ReadFile(backend.Path + "/bento.json") 137 | if err != nil { 138 | return err 139 | } 140 | 141 | err = json.Unmarshal(data, &backend.Config) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (backend *Backend) Start() error { 150 | err := backend.findBackend() 151 | if err != nil { 152 | return err 153 | } 154 | 155 | err = backend.readBentoConfig() 156 | if err != nil { 157 | return err 158 | } 159 | 160 | // TODO: Need a more reliable way to pick the port. 161 | backend.Port = 50000 + rand.Intn(1000) 162 | 163 | // TODO: This is problematic for arguments that have spaces. 164 | cmdParts := strings.Split(backend.Config.Run, " ") 165 | 166 | // It starts in the background. 167 | cmd := exec.Command(cmdParts[0], cmdParts[1:]...) 168 | cmd.Env = append(cmd.Env, fmt.Sprintf("BENTO_PORT=%d", backend.Port)) 169 | cmd.Dir = backend.Path 170 | 171 | if err := cmd.Start(); err != nil { 172 | return err 173 | } 174 | 175 | // TODO: Remove this? 176 | time.Sleep(time.Second) 177 | 178 | err = backend.connect() 179 | if err != nil { 180 | return err 181 | } 182 | 183 | err = backend.loadSentences() 184 | if err != nil { 185 | return err 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func (backend *Backend) String() string { 192 | return backend.Name 193 | } 194 | -------------------------------------------------------------------------------- /backend/example-scores-php/bento.json: -------------------------------------------------------------------------------- 1 | { 2 | "run": "php scores.php" 3 | } 4 | -------------------------------------------------------------------------------- /backend/example-scores-php/scores.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare scores is example-scores-php 3 | declare avg is number 4 | 5 | add 53.5 to scores 6 | add 17 to scores 7 | 8 | average of scores into avg 9 | 10 | display avg 11 | display scores 12 | -------------------------------------------------------------------------------- /backend/example-scores-php/scores.php: -------------------------------------------------------------------------------- 1 | function($args) use (&$total, &$count) { 7 | $total += $args[0]; 8 | ++$count; 9 | }, 10 | 'average of ? into ?' => function($args) use (&$total, &$count) { 11 | return ["set" => ['$1' => (string)($total / $count)]]; 12 | }, 13 | 'display ?' => function() use (&$total) { 14 | return ["text" => "The total is $total."]; 15 | } 16 | ]; 17 | 18 | // The code following should not need to be changed. 19 | 20 | $socket = socket_create(AF_INET, SOCK_STREAM, 0); 21 | $result = socket_bind($socket, "127.0.0.1", $_ENV['BENTO_PORT']); 22 | $result = socket_listen($socket, 3); 23 | $spawn = socket_accept($socket); 24 | 25 | while ($message = json_decode(socket_read($spawn, 65536, PHP_NORMAL_READ))) { 26 | if ($message->special === "sentences") { 27 | $result = ['sentences' => array_keys($handlers)]; 28 | } else { 29 | $handler = $handlers[$message->sentence]; 30 | $result = $handler($message->args); 31 | } 32 | 33 | if (!$result) { 34 | $result = new stdClass(); 35 | } 36 | 37 | $output = json_encode($result) . "\n"; 38 | socket_write($spawn, $output, strlen($output)); 39 | } 40 | 41 | socket_close($spawn); 42 | socket_close($socket); 43 | -------------------------------------------------------------------------------- /compiler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type CompiledFunction struct { 4 | Variables []interface{} 5 | Instructions []Instruction 6 | InstructionOffset int 7 | } 8 | 9 | type CompiledProgram struct { 10 | Functions map[string]*CompiledFunction 11 | } 12 | 13 | type Compiler struct { 14 | program *Program 15 | function *Function 16 | cf *CompiledFunction 17 | } 18 | 19 | func NewCompiler(program *Program) *Compiler { 20 | return &Compiler{ 21 | program: program, 22 | } 23 | } 24 | 25 | func (compiler *Compiler) Compile() *CompiledProgram { 26 | cp := &CompiledProgram{ 27 | Functions: make(map[string]*CompiledFunction), 28 | } 29 | 30 | for _, compiler.function = range compiler.program.Functions { 31 | syntax := compiler.function.Definition.Syntax() 32 | compiler.compileFunction() 33 | cp.Functions[syntax] = compiler.cf 34 | } 35 | 36 | return cp 37 | } 38 | 39 | func (compiler *Compiler) compileFunction() { 40 | compiler.cf = &CompiledFunction{} 41 | 42 | // Make spaces for the arguments and locally declared variables. These 43 | // placeholders will be nil. The virtual machine will fill in the real 44 | // values at the time the function is invoked. 45 | for _, variable := range compiler.function.Variables { 46 | var value interface{} 47 | 48 | switch variable.Type { 49 | case VariableTypeText: 50 | value = NewText("") 51 | 52 | case VariableTypeNumber: 53 | value = NewNumber("0", variable.Precision) 54 | 55 | default: 56 | value = NewBackend(variable.Type) 57 | } 58 | 59 | compiler.cf.Variables = append(compiler.cf.Variables, value) 60 | } 61 | 62 | // All of other constants are appended into the end. 63 | // TODO: Change this switch into an interface. 64 | for _, statement := range compiler.function.Statements { 65 | compiler.cf.Instructions = append(compiler.cf.Instructions, 66 | compiler.compileStatement(statement)...) 67 | } 68 | } 69 | 70 | func (compiler *Compiler) compileStatement(statement Statement) []Instruction { 71 | switch stmt := statement.(type) { 72 | case *Sentence: 73 | return []Instruction{compiler.compileSentence(stmt)} 74 | 75 | case *If: 76 | return compiler.compileIf(stmt) 77 | 78 | case *While: 79 | return compiler.compileWhile(stmt) 80 | 81 | case *QuestionAnswer: 82 | return []Instruction{compiler.compileQuestionAnswer(stmt)} 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (compiler *Compiler) resolveArg(arg interface{}) int { 89 | switch a := arg.(type) { 90 | case VariableReference: 91 | if a == BlackholeVariable { 92 | return blackholeVariableIndex 93 | } 94 | 95 | for i, arg2 := range compiler.function.Variables { 96 | if string(a) == arg2.Name { 97 | return i 98 | } 99 | } 100 | 101 | // TODO: handle bad variable name 102 | 103 | case *string, *Number: 104 | compiler.cf.Variables = append(compiler.cf.Variables, a) 105 | return len(compiler.cf.Variables) - 1 106 | } 107 | 108 | // Not possible 109 | return 0 110 | } 111 | 112 | func (compiler *Compiler) compileQuestionAnswer(answer *QuestionAnswer) Instruction { 113 | return &QuestionAnswerInstruction{ 114 | Yes: answer.Yes, 115 | } 116 | } 117 | 118 | func (compiler *Compiler) compileSentence(sentence *Sentence) Instruction { 119 | instruction := &CallInstruction{ 120 | Call: sentence.Syntax(), 121 | Args: nil, 122 | } 123 | 124 | // TODO: Check the syntax exists in the system or file. 125 | 126 | for _, arg := range sentence.Args() { 127 | instruction.Args = append(instruction.Args, compiler.resolveArg(arg)) 128 | } 129 | 130 | return instruction 131 | } 132 | 133 | func (compiler *Compiler) compileIf(ifStmt *If) (instructions []Instruction) { 134 | var jumpInstruction Instruction 135 | 136 | if ifStmt.Condition != nil { 137 | jumpInstruction = &ConditionJumpInstruction{ 138 | True: 1, 139 | False: 2, 140 | Operator: ifStmt.Condition.Operator, 141 | Left: compiler.resolveArg(ifStmt.Condition.Left), 142 | Right: compiler.resolveArg(ifStmt.Condition.Right), 143 | } 144 | } else { 145 | jumpInstruction = &QuestionJumpInstruction{ 146 | True: 1, 147 | False: 2, 148 | } 149 | } 150 | 151 | if ifStmt.False != nil { 152 | // This is to compensate for the added JumpInstruction that has to be 153 | // added below. 154 | if j, ok := jumpInstruction.(*ConditionJumpInstruction); ok { 155 | j.False++ 156 | } 157 | 158 | if j, ok := jumpInstruction.(*QuestionJumpInstruction); ok { 159 | j.False++ 160 | } 161 | } 162 | 163 | if ifStmt.Unless { 164 | if j, ok := jumpInstruction.(*ConditionJumpInstruction); ok { 165 | j.True, j.False = j.False, j.True 166 | } 167 | 168 | if j, ok := jumpInstruction.(*QuestionJumpInstruction); ok { 169 | j.True, j.False = j.False, j.True 170 | } 171 | } 172 | 173 | // If it's a question we need to ask it before we can use the answer. 174 | if ifStmt.Question != nil { 175 | instructions = append(instructions, 176 | compiler.compileSentence(ifStmt.Question)) 177 | } 178 | 179 | instructions = append(instructions, jumpInstruction) 180 | instructions = append(instructions, 181 | compiler.compileStatement(ifStmt.True)...) 182 | 183 | if ifStmt.False != nil { 184 | // This prevents the True case above from also running the else clause. 185 | instructions = append(instructions, &JumpInstruction{Forward: 2}) 186 | 187 | instructions = append(instructions, 188 | compiler.compileStatement(ifStmt.False)...) 189 | } 190 | 191 | return instructions 192 | } 193 | 194 | func (compiler *Compiler) compileWhile(whileStmt *While) []Instruction { 195 | jumpInstruction := &ConditionJumpInstruction{ 196 | Operator: whileStmt.Condition.Operator, 197 | True: 1, 198 | False: 3, 199 | } 200 | 201 | jumpInstruction.Left = compiler.resolveArg(whileStmt.Condition.Left) 202 | jumpInstruction.Right = compiler.resolveArg(whileStmt.Condition.Right) 203 | 204 | if whileStmt.Until { 205 | jumpInstruction.True, jumpInstruction.False = 206 | jumpInstruction.False, jumpInstruction.True 207 | } 208 | 209 | instructions := []Instruction{ 210 | jumpInstruction, 211 | compiler.compileSentence(whileStmt.True), 212 | &JumpInstruction{Forward: -2}, 213 | } 214 | 215 | return instructions 216 | } 217 | -------------------------------------------------------------------------------- /compiler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | "github.com/google/go-cmp/cmp/cmpopts" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | var compileTests = map[string]struct { 11 | program *Program 12 | expected *CompiledProgram 13 | }{ 14 | "Display": { 15 | program: &Program{ 16 | Functions: map[string]*Function{ 17 | "start": { 18 | Definition: &Sentence{Words: []interface{}{"start"}}, 19 | Statements: []Statement{ 20 | &Sentence{ 21 | Words: []interface{}{ 22 | "display", NewText("hello"), 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | expected: &CompiledProgram{ 30 | Functions: map[string]*CompiledFunction{ 31 | "start": { 32 | Variables: []interface{}{ 33 | NewText("hello"), 34 | }, 35 | Instructions: []Instruction{ 36 | &CallInstruction{ 37 | Call: "display ?", 38 | Args: []int{0}, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | "DisplayVariable": { 46 | program: &Program{ 47 | Functions: map[string]*Function{ 48 | "start": { 49 | Definition: &Sentence{Words: []interface{}{"start"}}, 50 | Variables: []*VariableDefinition{ 51 | { 52 | Name: "name", 53 | Type: "text", 54 | }, 55 | }, 56 | Statements: []Statement{ 57 | &Sentence{ 58 | Words: []interface{}{ 59 | "display", VariableReference("name"), 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | expected: &CompiledProgram{ 67 | Functions: map[string]*CompiledFunction{ 68 | "start": { 69 | Variables: []interface{}{ 70 | NewText(""), 71 | }, 72 | Instructions: []Instruction{ 73 | &CallInstruction{ 74 | Call: "display ?", 75 | Args: []int{0}, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | "DisplayVariable2": { 83 | program: &Program{ 84 | Functions: map[string]*Function{ 85 | "start": { 86 | Definition: &Sentence{Words: []interface{}{"start"}}, 87 | Variables: []*VariableDefinition{ 88 | { 89 | Name: "name", 90 | Type: "text", 91 | LocalScope: true, 92 | }, 93 | }, 94 | Statements: []Statement{ 95 | &Sentence{ 96 | Words: []interface{}{ 97 | "display", NewText("hi"), 98 | }, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | expected: &CompiledProgram{ 105 | Functions: map[string]*CompiledFunction{ 106 | "start": { 107 | Variables: []interface{}{ 108 | NewText(""), NewText("hi"), 109 | }, 110 | Instructions: []Instruction{ 111 | &CallInstruction{ 112 | Call: "display ?", 113 | Args: []int{1}, 114 | }, 115 | }, 116 | }, 117 | }, 118 | }, 119 | }, 120 | "Display2": { 121 | program: &Program{ 122 | Functions: map[string]*Function{ 123 | "start": { 124 | Definition: &Sentence{Words: []interface{}{"start"}}, 125 | Variables: []*VariableDefinition{ 126 | { 127 | Name: "name", 128 | Type: "text", 129 | LocalScope: true, 130 | }, 131 | }, 132 | Statements: []Statement{ 133 | &Sentence{ 134 | Words: []interface{}{ 135 | "display", NewText("hi"), 136 | }, 137 | }, 138 | &Sentence{ 139 | Words: []interface{}{ 140 | "set", NewText("foo"), "to", VariableReference("name"), 141 | }, 142 | }, 143 | }, 144 | }, 145 | }, 146 | }, 147 | expected: &CompiledProgram{ 148 | Functions: map[string]*CompiledFunction{ 149 | "start": { 150 | Variables: []interface{}{ 151 | NewText(""), NewText("hi"), NewText("foo"), 152 | }, 153 | Instructions: []Instruction{ 154 | &CallInstruction{ 155 | Call: "display ?", 156 | Args: []int{1}, 157 | }, 158 | &CallInstruction{ 159 | Call: "set ? to ?", 160 | Args: []int{2, 0}, 161 | }, 162 | }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | "CallFunctionWithoutArguments": { 168 | program: &Program{ 169 | Functions: map[string]*Function{ 170 | "start": { 171 | Definition: &Sentence{Words: []interface{}{"start"}}, 172 | Statements: []Statement{ 173 | &Sentence{ 174 | Words: []interface{}{ 175 | "print", 176 | }, 177 | }, 178 | }, 179 | }, 180 | "print": { 181 | Definition: &Sentence{Words: []interface{}{"print"}}, 182 | Statements: []Statement{ 183 | &Sentence{ 184 | Words: []interface{}{ 185 | "display", NewText("hi"), 186 | }, 187 | }, 188 | }, 189 | }, 190 | }, 191 | }, 192 | expected: &CompiledProgram{ 193 | Functions: map[string]*CompiledFunction{ 194 | "start": { 195 | Instructions: []Instruction{ 196 | &CallInstruction{ 197 | Call: "print", 198 | }, 199 | }, 200 | }, 201 | "print": { 202 | Variables: []interface{}{ 203 | NewText("hi"), 204 | }, 205 | Instructions: []Instruction{ 206 | &CallInstruction{ 207 | Call: "display ?", 208 | Args: []int{0}, 209 | }, 210 | }, 211 | }, 212 | }, 213 | }, 214 | }, 215 | "CallFunctionWithArguments": { 216 | program: &Program{ 217 | Functions: map[string]*Function{ 218 | "start": { 219 | Definition: &Sentence{Words: []interface{}{"start"}}, 220 | Statements: []Statement{ 221 | &Sentence{ 222 | Words: []interface{}{ 223 | "print", NewText("foo"), 224 | }, 225 | }, 226 | }, 227 | }, 228 | "print ?": { 229 | Definition: &Sentence{Words: []interface{}{ 230 | "print", VariableReference("message"), 231 | }}, 232 | Variables: []*VariableDefinition{ 233 | { 234 | Name: "message", 235 | Type: "text", 236 | }, 237 | }, 238 | Statements: []Statement{ 239 | &Sentence{ 240 | Words: []interface{}{ 241 | "display", VariableReference("message"), 242 | }, 243 | }, 244 | }, 245 | }, 246 | }, 247 | }, 248 | expected: &CompiledProgram{ 249 | Functions: map[string]*CompiledFunction{ 250 | "start": { 251 | Variables: []interface{}{ 252 | NewText("foo"), 253 | }, 254 | Instructions: []Instruction{ 255 | &CallInstruction{ 256 | Call: "print ?", 257 | Args: []int{0}, 258 | }, 259 | }, 260 | }, 261 | "print ?": { 262 | Variables: []interface{}{ 263 | NewText(""), 264 | }, 265 | Instructions: []Instruction{ 266 | &CallInstruction{ 267 | Call: "display ?", 268 | Args: []int{0}, 269 | }, 270 | }, 271 | }, 272 | }, 273 | }, 274 | }, 275 | "NumberVariable": { 276 | program: &Program{ 277 | Functions: map[string]*Function{ 278 | "start": { 279 | Definition: &Sentence{Words: []interface{}{"start"}}, 280 | Variables: []*VariableDefinition{ 281 | { 282 | Name: "num", 283 | Type: "number", 284 | }, 285 | }, 286 | Statements: []Statement{ 287 | &Sentence{ 288 | Words: []interface{}{ 289 | "display", VariableReference("num"), 290 | }, 291 | }, 292 | }, 293 | }, 294 | }, 295 | }, 296 | expected: &CompiledProgram{ 297 | Functions: map[string]*CompiledFunction{ 298 | "start": { 299 | Variables: []interface{}{ 300 | NewNumber("0", DefaultNumericPrecision), 301 | }, 302 | Instructions: []Instruction{ 303 | &CallInstruction{ 304 | Call: "display ?", 305 | Args: []int{0}, 306 | }, 307 | }, 308 | }, 309 | }, 310 | }, 311 | }, 312 | "InlineIf": { 313 | program: &Program{ 314 | Functions: map[string]*Function{ 315 | "start": { 316 | Definition: &Sentence{Words: []interface{}{"start"}}, 317 | Statements: []Statement{ 318 | &If{ 319 | Condition: &Condition{ 320 | Left: NewText("foo"), 321 | Right: NewText("bar"), 322 | Operator: OperatorEqual, 323 | }, 324 | True: &Sentence{ 325 | Words: []interface{}{ 326 | "display", NewText("match!"), 327 | }, 328 | }, 329 | }, 330 | &Sentence{ 331 | Words: []interface{}{ 332 | "display", NewText("done"), 333 | }, 334 | }, 335 | }, 336 | }, 337 | }, 338 | }, 339 | expected: &CompiledProgram{ 340 | Functions: map[string]*CompiledFunction{ 341 | "start": { 342 | Variables: []interface{}{ 343 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 344 | }, 345 | Instructions: []Instruction{ 346 | &ConditionJumpInstruction{ 347 | Left: 0, 348 | Right: 1, 349 | Operator: OperatorEqual, 350 | True: 1, 351 | False: 2, 352 | }, 353 | &CallInstruction{ 354 | Call: "display ?", 355 | Args: []int{2}, 356 | }, 357 | &CallInstruction{ 358 | Call: "display ?", 359 | Args: []int{3}, 360 | }, 361 | }, 362 | }, 363 | }, 364 | }, 365 | }, 366 | "InlineIfElse": { 367 | program: &Program{ 368 | Functions: map[string]*Function{ 369 | "start": { 370 | Definition: &Sentence{Words: []interface{}{"start"}}, 371 | Statements: []Statement{ 372 | &If{ 373 | Condition: &Condition{ 374 | Left: NewText("foo"), 375 | Right: NewText("bar"), 376 | Operator: OperatorNotEqual, 377 | }, 378 | True: &Sentence{ 379 | Words: []interface{}{ 380 | "display", NewText("match!"), 381 | }, 382 | }, 383 | False: &Sentence{ 384 | Words: []interface{}{ 385 | "display", NewText("no match!"), 386 | }, 387 | }, 388 | }, 389 | &Sentence{ 390 | Words: []interface{}{ 391 | "display", NewText("done"), 392 | }, 393 | }, 394 | }, 395 | }, 396 | }, 397 | }, 398 | expected: &CompiledProgram{ 399 | Functions: map[string]*CompiledFunction{ 400 | "start": { 401 | Variables: []interface{}{ 402 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("no match!"), NewText("done"), 403 | }, 404 | Instructions: []Instruction{ 405 | &ConditionJumpInstruction{ 406 | Left: 0, 407 | Right: 1, 408 | Operator: OperatorNotEqual, 409 | True: 1, 410 | False: 3, 411 | }, 412 | &CallInstruction{ 413 | Call: "display ?", 414 | Args: []int{2}, 415 | }, 416 | &JumpInstruction{ 417 | Forward: 2, 418 | }, 419 | &CallInstruction{ 420 | Call: "display ?", 421 | Args: []int{3}, 422 | }, 423 | &CallInstruction{ 424 | Call: "display ?", 425 | Args: []int{4}, 426 | }, 427 | }, 428 | }, 429 | }, 430 | }, 431 | }, 432 | "InlineUnless": { 433 | program: &Program{ 434 | Functions: map[string]*Function{ 435 | "start": { 436 | Definition: &Sentence{Words: []interface{}{"start"}}, 437 | Statements: []Statement{ 438 | &If{ 439 | Unless: true, 440 | Condition: &Condition{ 441 | Left: NewText("foo"), 442 | Right: NewText("bar"), 443 | Operator: OperatorEqual, 444 | }, 445 | True: &Sentence{ 446 | Words: []interface{}{ 447 | "display", NewText("match!"), 448 | }, 449 | }, 450 | }, 451 | &Sentence{ 452 | Words: []interface{}{ 453 | "display", NewText("done"), 454 | }, 455 | }, 456 | }, 457 | }, 458 | }, 459 | }, 460 | expected: &CompiledProgram{ 461 | Functions: map[string]*CompiledFunction{ 462 | "start": { 463 | Variables: []interface{}{ 464 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 465 | }, 466 | Instructions: []Instruction{ 467 | &ConditionJumpInstruction{ 468 | Left: 0, 469 | Right: 1, 470 | Operator: OperatorEqual, 471 | True: 2, 472 | False: 1, 473 | }, 474 | &CallInstruction{ 475 | Call: "display ?", 476 | Args: []int{2}, 477 | }, 478 | &CallInstruction{ 479 | Call: "display ?", 480 | Args: []int{3}, 481 | }, 482 | }, 483 | }, 484 | }, 485 | }, 486 | }, 487 | "InlineUnlessElse": { 488 | program: &Program{ 489 | Functions: map[string]*Function{ 490 | "start": { 491 | Definition: &Sentence{Words: []interface{}{"start"}}, 492 | Statements: []Statement{ 493 | &If{ 494 | Unless: true, 495 | Condition: &Condition{ 496 | Left: NewText("foo"), 497 | Right: NewText("bar"), 498 | Operator: OperatorNotEqual, 499 | }, 500 | True: &Sentence{ 501 | Words: []interface{}{ 502 | "display", NewText("match!"), 503 | }, 504 | }, 505 | False: &Sentence{ 506 | Words: []interface{}{ 507 | "display", NewText("no match!"), 508 | }, 509 | }, 510 | }, 511 | &Sentence{ 512 | Words: []interface{}{ 513 | "display", NewText("done"), 514 | }, 515 | }, 516 | }, 517 | }, 518 | }, 519 | }, 520 | expected: &CompiledProgram{ 521 | Functions: map[string]*CompiledFunction{ 522 | "start": { 523 | Variables: []interface{}{ 524 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("no match!"), NewText("done"), 525 | }, 526 | Instructions: []Instruction{ 527 | &ConditionJumpInstruction{ 528 | Left: 0, 529 | Right: 1, 530 | Operator: OperatorNotEqual, 531 | True: 3, 532 | False: 1, 533 | }, 534 | &CallInstruction{ 535 | Call: "display ?", 536 | Args: []int{2}, 537 | }, 538 | &JumpInstruction{ 539 | Forward: 2, 540 | }, 541 | &CallInstruction{ 542 | Call: "display ?", 543 | Args: []int{3}, 544 | }, 545 | &CallInstruction{ 546 | Call: "display ?", 547 | Args: []int{4}, 548 | }, 549 | }, 550 | }, 551 | }, 552 | }, 553 | }, 554 | "InlineWhile": { 555 | program: &Program{ 556 | Functions: map[string]*Function{ 557 | "start": { 558 | Definition: &Sentence{Words: []interface{}{"start"}}, 559 | Statements: []Statement{ 560 | &While{ 561 | Condition: &Condition{ 562 | Left: NewText("foo"), 563 | Right: NewText("bar"), 564 | Operator: OperatorEqual, 565 | }, 566 | True: &Sentence{ 567 | Words: []interface{}{ 568 | "display", NewText("match!"), 569 | }, 570 | }, 571 | }, 572 | &Sentence{ 573 | Words: []interface{}{ 574 | "display", NewText("done"), 575 | }, 576 | }, 577 | }, 578 | }, 579 | }, 580 | }, 581 | expected: &CompiledProgram{ 582 | Functions: map[string]*CompiledFunction{ 583 | "start": { 584 | Variables: []interface{}{ 585 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 586 | }, 587 | Instructions: []Instruction{ 588 | &ConditionJumpInstruction{ 589 | Left: 0, 590 | Right: 1, 591 | Operator: OperatorEqual, 592 | True: 1, 593 | False: 3, 594 | }, 595 | &CallInstruction{ 596 | Call: "display ?", 597 | Args: []int{2}, 598 | }, 599 | &JumpInstruction{ 600 | Forward: -2, 601 | }, 602 | &CallInstruction{ 603 | Call: "display ?", 604 | Args: []int{3}, 605 | }, 606 | }, 607 | }, 608 | }, 609 | }, 610 | }, 611 | "InlineUntil": { 612 | program: &Program{ 613 | Functions: map[string]*Function{ 614 | "start": { 615 | Definition: &Sentence{Words: []interface{}{"start"}}, 616 | Statements: []Statement{ 617 | &While{ 618 | Until: true, 619 | Condition: &Condition{ 620 | Left: NewText("foo"), 621 | Right: NewText("bar"), 622 | Operator: OperatorEqual, 623 | }, 624 | True: &Sentence{ 625 | Words: []interface{}{ 626 | "display", NewText("match!"), 627 | }, 628 | }, 629 | }, 630 | &Sentence{ 631 | Words: []interface{}{ 632 | "display", NewText("done"), 633 | }, 634 | }, 635 | }, 636 | }, 637 | }, 638 | }, 639 | expected: &CompiledProgram{ 640 | Functions: map[string]*CompiledFunction{ 641 | "start": { 642 | Variables: []interface{}{ 643 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 644 | }, 645 | Instructions: []Instruction{ 646 | &ConditionJumpInstruction{ 647 | Left: 0, 648 | Right: 1, 649 | Operator: OperatorEqual, 650 | True: 3, 651 | False: 1, 652 | }, 653 | &CallInstruction{ 654 | Call: "display ?", 655 | Args: []int{2}, 656 | }, 657 | &JumpInstruction{ 658 | Forward: -2, 659 | }, 660 | &CallInstruction{ 661 | Call: "display ?", 662 | Args: []int{3}, 663 | }, 664 | }, 665 | }, 666 | }, 667 | }, 668 | }, 669 | "NumberWithPrecision": { 670 | program: &Program{ 671 | Functions: map[string]*Function{ 672 | "start": { 673 | Definition: &Sentence{Words: []interface{}{"start"}}, 674 | Variables: []*VariableDefinition{ 675 | { 676 | Name: "foo", 677 | Type: "number", 678 | LocalScope: true, 679 | Precision: 2, 680 | }, 681 | }, 682 | Statements: []Statement{ 683 | &Sentence{ 684 | Words: []interface{}{ 685 | "display", VariableReference("foo"), 686 | }, 687 | }, 688 | }, 689 | }, 690 | }, 691 | }, 692 | expected: &CompiledProgram{ 693 | Functions: map[string]*CompiledFunction{ 694 | "start": { 695 | Variables: []interface{}{ 696 | NewNumber("0", 2), 697 | }, 698 | Instructions: []Instruction{ 699 | &CallInstruction{ 700 | Call: "display ?", 701 | Args: []int{0}, 702 | }, 703 | }, 704 | }, 705 | }, 706 | }, 707 | }, 708 | "DisplayBlackhole": { 709 | program: &Program{ 710 | Functions: map[string]*Function{ 711 | "start": { 712 | Definition: &Sentence{Words: []interface{}{"start"}}, 713 | Statements: []Statement{ 714 | &Sentence{ 715 | Words: []interface{}{ 716 | "display", VariableReference("_"), 717 | }, 718 | }, 719 | }, 720 | }, 721 | }, 722 | }, 723 | expected: &CompiledProgram{ 724 | Functions: map[string]*CompiledFunction{ 725 | "start": { 726 | Instructions: []Instruction{ 727 | &CallInstruction{ 728 | Call: "display ?", 729 | Args: []int{-1}, 730 | }, 731 | }, 732 | }, 733 | }, 734 | }, 735 | }, 736 | "QuestionDefinition1Yes": { 737 | program: &Program{ 738 | Functions: map[string]*Function{ 739 | "is good": { 740 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 741 | Statements: []Statement{ 742 | &QuestionAnswer{ 743 | Yes: true, 744 | }, 745 | }, 746 | }, 747 | }, 748 | }, 749 | expected: &CompiledProgram{ 750 | Functions: map[string]*CompiledFunction{ 751 | "is good": { 752 | Instructions: []Instruction{ 753 | &QuestionAnswerInstruction{ 754 | Yes: true, 755 | }, 756 | }, 757 | }, 758 | }, 759 | }, 760 | }, 761 | "QuestionDefinition1No": { 762 | program: &Program{ 763 | Functions: map[string]*Function{ 764 | "is good": { 765 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 766 | Statements: []Statement{ 767 | &QuestionAnswer{ 768 | Yes: false, 769 | }, 770 | }, 771 | }, 772 | }, 773 | }, 774 | expected: &CompiledProgram{ 775 | Functions: map[string]*CompiledFunction{ 776 | "is good": { 777 | Instructions: []Instruction{ 778 | &QuestionAnswerInstruction{ 779 | Yes: false, 780 | }, 781 | }, 782 | }, 783 | }, 784 | }, 785 | }, 786 | "QuestionDefinition2YesAndOtherWords": { 787 | program: &Program{ 788 | Functions: map[string]*Function{ 789 | "is good": { 790 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 791 | Statements: []Statement{ 792 | &Sentence{ 793 | Words: []interface{}{ 794 | "yes", "then", 795 | }, 796 | }, 797 | }, 798 | }, 799 | }, 800 | }, 801 | expected: &CompiledProgram{ 802 | Functions: map[string]*CompiledFunction{ 803 | "is good": { 804 | Instructions: []Instruction{ 805 | &CallInstruction{ 806 | Call: "yes then", 807 | }, 808 | }, 809 | }, 810 | }, 811 | }, 812 | }, 813 | "QuestionDefinitionWithVars": { 814 | program: &Program{ 815 | Functions: map[string]*Function{ 816 | "? is good": { 817 | Definition: &Sentence{Words: []interface{}{ 818 | VariableReference("foo"), "is", "good"}, 819 | }, 820 | Variables: []*VariableDefinition{ 821 | { 822 | Name: "foo", 823 | Type: "text", 824 | }, 825 | }, 826 | Statements: []Statement{ 827 | &QuestionAnswer{ 828 | Yes: true, 829 | }, 830 | }, 831 | }, 832 | }, 833 | }, 834 | expected: &CompiledProgram{ 835 | Functions: map[string]*CompiledFunction{ 836 | "? is good": { 837 | Variables: []interface{}{ 838 | NewText(""), 839 | }, 840 | Instructions: []Instruction{ 841 | &QuestionAnswerInstruction{ 842 | Yes: true, 843 | }, 844 | }, 845 | }, 846 | }, 847 | }, 848 | }, 849 | "AnswerInNormalFunction": { 850 | program: &Program{ 851 | Functions: map[string]*Function{ 852 | "? is good": { 853 | Definition: &Sentence{Words: []interface{}{ 854 | VariableReference("foo"), "is", "good"}, 855 | }, 856 | Variables: []*VariableDefinition{ 857 | { 858 | Name: "foo", 859 | Type: "text", 860 | }, 861 | }, 862 | Statements: []Statement{ 863 | &Sentence{ 864 | Words: []interface{}{"yes"}, 865 | }, 866 | }, 867 | }, 868 | }, 869 | }, 870 | expected: &CompiledProgram{ 871 | Functions: map[string]*CompiledFunction{ 872 | "? is good": { 873 | Variables: []interface{}{ 874 | NewText(""), 875 | }, 876 | Instructions: []Instruction{ 877 | &CallInstruction{ 878 | Call: "yes", 879 | }, 880 | }, 881 | }, 882 | }, 883 | }, 884 | }, 885 | "IfQuestion": { 886 | program: &Program{ 887 | Functions: map[string]*Function{ 888 | "start": { 889 | Definition: &Sentence{Words: []interface{}{"start"}}, 890 | Statements: []Statement{ 891 | &If{ 892 | Question: &Sentence{ 893 | Words: []interface{}{"something", "is", "true"}, 894 | }, 895 | True: &Sentence{ 896 | Words: []interface{}{"all", "good"}, 897 | }, 898 | }, 899 | }, 900 | }, 901 | "something is true": { 902 | IsQuestion: true, 903 | Definition: &Sentence{ 904 | Words: []interface{}{"something", "is", "true"}, 905 | }, 906 | Statements: []Statement{ 907 | &QuestionAnswer{ 908 | Yes: true, 909 | }, 910 | }, 911 | }, 912 | }, 913 | }, 914 | expected: &CompiledProgram{ 915 | Functions: map[string]*CompiledFunction{ 916 | "start": { 917 | Instructions: []Instruction{ 918 | &CallInstruction{ 919 | Call: "something is true", 920 | }, 921 | &QuestionJumpInstruction{ 922 | True: 1, 923 | False: 2, 924 | }, 925 | &CallInstruction{ 926 | Call: "all good", 927 | }, 928 | }, 929 | }, 930 | "something is true": { 931 | Instructions: []Instruction{ 932 | &QuestionAnswerInstruction{ 933 | Yes: true, 934 | }, 935 | }, 936 | }, 937 | }, 938 | }, 939 | }, 940 | } 941 | 942 | func TestCompileProgram(t *testing.T) { 943 | for testName, test := range compileTests { 944 | t.Run(testName, func(t *testing.T) { 945 | compiler := NewCompiler(test.program) 946 | cf := compiler.Compile() 947 | 948 | diff := cmp.Diff(test.expected, cf, 949 | cmpopts.IgnoreTypes((func([]interface{}))(nil)), 950 | cmpopts.AcyclicTransformer("NumberToString", 951 | func(number *Number) string { 952 | return number.String() 953 | })) 954 | 955 | assert.Empty(t, diff) 956 | }) 957 | } 958 | } 959 | -------------------------------------------------------------------------------- /examples/am-pm.bento: -------------------------------------------------------------------------------- 1 | Start: 2 | Say hello to "Jane" 3 | Say hello to "David" 4 | 5 | Say hello to persons-name (persons-name is text): 6 | If it is the afternoon, 7 | display "Good afternoon, " persons-name "!", 8 | otherwise display "Good morning, " persons-name "!" 9 | 10 | It is the afternoon? 11 | Declare am-or-pm is text 12 | Run system command "printf `date +'%p'`" output into am-or-pm 13 | If am-or-pm = "PM", yes 14 | -------------------------------------------------------------------------------- /examples/custom-sentences.bento: -------------------------------------------------------------------------------- 1 | start: 2 | print everything 3 | say "Hi" to "Bob" 4 | 5 | print everything: 6 | display "Hello" 7 | display "World" 8 | 9 | say greeting to persons-name (persons-name is text, greeting is text): 10 | display greeting 11 | display persons-name 12 | -------------------------------------------------------------------------------- /examples/hello-world.bento: -------------------------------------------------------------------------------- 1 | # Comments start with a hash. 2 | 3 | start: 4 | display "Hello, World!" 5 | -------------------------------------------------------------------------------- /examples/variables.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare first-var is text 3 | declare second-var is text 4 | 5 | set first-var to "hello" 6 | 7 | display first-var 8 | display second-var 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elliotchance/bento 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/google/go-cmp v0.3.0 7 | github.com/stretchr/testify v1.3.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 4 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | ) 10 | 11 | var ( 12 | flagAst bool 13 | ) 14 | 15 | func main() { 16 | flag.BoolVar(&flagAst, "ast", false, "Print out the parsed AST and "+ 17 | "exist. This is useful for debugging, but you should not assume that "+ 18 | "the format returned will be consistent or if -ast will remain in "+ 19 | "any future version.") 20 | flag.Parse() 21 | 22 | for _, arg := range flag.Args() { 23 | file, err := os.Open(arg) 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | 28 | parser := NewParser(file) 29 | program, err := parser.Parse() 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | 34 | if flagAst { 35 | data, err := json.MarshalIndent(program, "", " ") 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | fmt.Println(string(data)) 40 | os.Exit(0) 41 | } 42 | 43 | compiler := NewCompiler(program) 44 | compiledProgram := compiler.Compile() 45 | 46 | vm := NewVirtualMachine(compiledProgram) 47 | err = vm.Run() 48 | 49 | if err != nil { 50 | log.Fatalln(err) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "io/ioutil" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestBentoFiles(t *testing.T) { 15 | dir := "tests/" 16 | fileInfos, err := ioutil.ReadDir(dir) 17 | require.NoError(t, err) 18 | 19 | for _, fileInfo := range fileInfos { 20 | // This is useful for debugging a single file, so i'll leave it 21 | // commented out. 22 | //if fileInfo.Name() != "number.bento" { 23 | // continue 24 | //} 25 | 26 | if !strings.HasSuffix(fileInfo.Name(), ".bento") { 27 | continue 28 | } 29 | 30 | t.Run(fileInfo.Name(), func(t *testing.T) { 31 | file, err := os.Open(dir + fileInfo.Name()) 32 | require.NoError(t, err) 33 | 34 | parser := NewParser(file) 35 | program, err := parser.Parse() 36 | require.NoError(t, err) 37 | 38 | compiler := NewCompiler(program) 39 | compiledProgram := compiler.Compile() 40 | 41 | vm := NewVirtualMachine(compiledProgram) 42 | vm.out = bytes.NewBuffer(nil) 43 | err = vm.Run() 44 | require.NoError(t, err) 45 | 46 | // There can be a specific expectation file depending on the OS. 47 | expectedFilePath := dir + strings.Replace(fileInfo.Name(), ".bento", "."+runtime.GOOS+".txt", -1) 48 | expectedData, err := ioutil.ReadFile(expectedFilePath) 49 | 50 | if err != nil { 51 | // Fallback to generic expectation file. 52 | expectedFilePath = dir + strings.Replace(fileInfo.Name(), ".bento", ".txt", -1) 53 | expectedData, err = ioutil.ReadFile(expectedFilePath) 54 | require.NoError(t, err) 55 | } 56 | 57 | assert.Equal(t, string(expectedData), vm.out.(*bytes.Buffer).String()) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/big" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | DefaultNumericPrecision = 6 10 | 11 | // TODO: This is a placeholder for when it can be handled more gracefully. 12 | // We need this for constants so that we don't lose precision. 13 | UnlimitedPrecision = 1000 14 | ) 15 | 16 | type Number struct { 17 | Rat *big.Rat 18 | Precision int 19 | } 20 | 21 | func NewNumber(s string, precision int) *Number { 22 | // TODO: The number of decimal places cannot be negative. 23 | 24 | rat, _ := big.NewRat(0, 1).SetString(s) 25 | 26 | return &Number{ 27 | Rat: rat, 28 | Precision: precision, 29 | } 30 | } 31 | 32 | func (number *Number) String() string { 33 | s := number.Rat.FloatString(number.Precision) 34 | 35 | // Remove any trailing zeros after the decimal point. 36 | if number.Precision > 0 { 37 | s = strings.TrimRight(s, "0") 38 | } 39 | 40 | // For integers we also want to remove the ".". 41 | return strings.TrimRight(s, ".") 42 | } 43 | 44 | func (number *Number) Cmp(number2 *Number) int { 45 | return number.Rat.Cmp(number2.Rat) 46 | } 47 | 48 | func (number *Number) Add(a, b *Number) { 49 | number.Rat = big.NewRat(0, 1).Add(a.Rat, b.Rat) 50 | } 51 | 52 | func (number *Number) Sub(a, b *Number) { 53 | number.Rat = big.NewRat(0, 1).Sub(a.Rat, b.Rat) 54 | } 55 | 56 | func (number *Number) Mul(a, b *Number) { 57 | result := big.NewRat(0, 1).Mul(a.Rat, b.Rat) 58 | 59 | // Multiplying two rational numbers may give a decimal that is higher 60 | // precision than we allow, so it has to be rounded before it's stored. 61 | number.Rat.SetString(result.FloatString(number.Precision)) 62 | } 63 | 64 | func (number *Number) Quo(a, b *Number) { 65 | result := big.NewRat(0, 1).Quo(a.Rat, b.Rat) 66 | 67 | // TODO: What about divide-by-zero? 68 | 69 | // Dividing two rational numbers may give a decimal that is higher precision 70 | // than we allow, so it has to be rounded before it's stored. 71 | number.Rat.SetString(result.FloatString(number.Precision)) 72 | } 73 | 74 | func (number *Number) Set(x *Number) { 75 | number.Rat.SetString(x.Rat.FloatString(number.Precision)) 76 | } 77 | 78 | func (number *Number) Bool() bool { 79 | return number.Rat.Sign() != 0 80 | } 81 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNewNumber(t *testing.T) { 9 | t.Run("Integer", func(t *testing.T) { 10 | assert.Equal(t, "123", NewNumber("123", 6).String()) 11 | }) 12 | 13 | t.Run("Float", func(t *testing.T) { 14 | assert.Equal(t, "1.23", NewNumber("1.23", 6).String()) 15 | }) 16 | 17 | t.Run("NegativeFloat", func(t *testing.T) { 18 | assert.Equal(t, "-1.23", NewNumber("-1.23", 6).String()) 19 | }) 20 | 21 | t.Run("LargeInteger", func(t *testing.T) { 22 | assert.Equal(t, "12300", NewNumber("12300", 6).String()) 23 | }) 24 | 25 | t.Run("RoundingDown", func(t *testing.T) { 26 | assert.Equal(t, "123.42", NewNumber("123.421", 2).String()) 27 | }) 28 | 29 | t.Run("RoundingUp", func(t *testing.T) { 30 | assert.Equal(t, "123.43", NewNumber("123.428", 2).String()) 31 | }) 32 | } 33 | 34 | func TestNumber_Mul(t *testing.T) { 35 | a := NewNumber("5.5", 1) 36 | b := NewNumber("6.5", 1) 37 | c := NewNumber("0", 1) 38 | c.Mul(a, b) // 35.75 -> 35.8 39 | assert.Equal(t, "35.8", c.String()) 40 | 41 | // To validate that it's not keeping a higher precision internally. 42 | c.Mul(c, NewNumber("11", 1)) 43 | 44 | // This would be 393.25 without correct rounding. 45 | assert.Equal(t, "393.8", c.String()) 46 | } 47 | 48 | func TestNumber_Quo(t *testing.T) { 49 | a := NewNumber("5.5", 1) 50 | b := NewNumber("6.5", 1) 51 | c := NewNumber("0", 2) 52 | c.Quo(a, b) // ~0.8461 = 0.85 53 | assert.Equal(t, "0.85", c.String()) 54 | 55 | // To validate that it's not keeping a higher precision internally. 56 | c.Mul(c, NewNumber("11", 1)) 57 | 58 | // This would be ~9.31 without correct rounding. 59 | assert.Equal(t, "9.35", c.String()) 60 | } 61 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | // TODO: Prevent a variable from being redefined by the same name in a function. 11 | 12 | // TODO: You cannot declare a variable with the same name as one of the function 13 | // parameters. 14 | 15 | // These reserved words have special meaning when they are the first word of the 16 | // sentence. It's fine to include them as normal words inside a sentence. 17 | const ( 18 | WordDeclare = "declare" 19 | WordIf = "if" 20 | WordOtherwise = "otherwise" 21 | WordUnless = "unless" 22 | WordUntil = "until" 23 | WordWhile = "while" 24 | ) 25 | 26 | type Parser struct { 27 | r io.Reader 28 | tokens []Token 29 | offset int 30 | program *Program 31 | } 32 | 33 | func NewParser(r io.Reader) *Parser { 34 | return &Parser{ 35 | r: r, 36 | } 37 | } 38 | 39 | func (parser *Parser) Parse() (*Program, error) { 40 | var err error 41 | parser.tokens, err = Tokenize(parser.r) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | parser.program = &Program{ 47 | Functions: map[string]*Function{}, 48 | } 49 | 50 | // Now we can compile the program. 51 | for !parser.isFinished() { 52 | function, err := parser.consumeFunction() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | parser.program.AppendFunction(function) 58 | } 59 | 60 | return parser.program, nil 61 | } 62 | 63 | func (parser *Parser) consumeToken(kind string) (Token, error) { 64 | if parser.offset >= len(parser.tokens) { 65 | return Token{}, 66 | errors.New("expected token, but the file ended unexpectedly") 67 | } 68 | 69 | if kind2 := parser.tokens[parser.offset].Kind; kind2 != kind { 70 | return Token{}, fmt.Errorf("expected %s, but got %s", kind, kind2) 71 | } 72 | 73 | token := parser.tokens[parser.offset] 74 | 75 | parser.offset++ 76 | 77 | // Consume a conditional multiline. 78 | if parser.offset+1 < len(parser.tokens) && 79 | parser.tokens[parser.offset].Kind == TokenKindEllipsis { 80 | if kind2 := parser.tokens[parser.offset+1].Kind; kind2 != TokenKindEndOfLine { 81 | return Token{}, 82 | fmt.Errorf("expected %s, but got %s", TokenKindEndOfLine, kind2) 83 | } 84 | 85 | parser.offset += 2 86 | } 87 | 88 | return token, nil 89 | } 90 | 91 | func (parser *Parser) consumeTokens(kinds []string) (tokens []Token) { 92 | for ; parser.offset < len(parser.tokens); parser.offset++ { 93 | token := parser.tokens[parser.offset] 94 | 95 | found := false 96 | for _, kind := range kinds { 97 | if kind == token.Kind { 98 | found = true 99 | break 100 | } 101 | } 102 | 103 | if !found { 104 | break 105 | } 106 | 107 | tokens = append(tokens, token) 108 | } 109 | 110 | return 111 | } 112 | 113 | func (parser *Parser) consumeSpecificWord(expected ...string) (word string, err error) { 114 | originalOffset := parser.offset 115 | defer func() { 116 | if err != nil { 117 | parser.offset = originalOffset 118 | } 119 | }() 120 | 121 | word, err = parser.consumeWord() 122 | if err != nil { 123 | return "", err 124 | } 125 | 126 | for _, allowed := range expected { 127 | if word == allowed { 128 | return word, nil 129 | } 130 | } 131 | 132 | return "", fmt.Errorf(`expected one of "%v", but got "%s"`, expected, word) 133 | } 134 | 135 | func (parser *Parser) consumeWord() (string, error) { 136 | token, err := parser.consumeToken(TokenKindWord) 137 | if err != nil { 138 | return "", err 139 | } 140 | 141 | return token.Value, err 142 | } 143 | 144 | func (parser *Parser) consumeSentenceWord(varMap map[string]*VariableDefinition) (_ interface{}, err error) { 145 | originalOffset := parser.offset 146 | defer func() { 147 | if err != nil { 148 | parser.offset = originalOffset 149 | } 150 | }() 151 | 152 | var token Token 153 | token, err = parser.consumeToken(TokenKindWord) 154 | if err == nil { 155 | if _, ok := varMap[token.Value]; ok || token.Value == "_" { 156 | return VariableReference(token.Value), nil 157 | } 158 | 159 | return token.Value, nil 160 | } 161 | 162 | token, err = parser.consumeToken(TokenKindText) 163 | if err == nil { 164 | return NewText(token.Value), nil 165 | } 166 | 167 | token, err = parser.consumeToken(TokenKindNumber) 168 | if err == nil { 169 | return NewNumber(token.Value, UnlimitedPrecision), nil 170 | } 171 | 172 | return nil, fmt.Errorf("expected sentence word, but found something else") 173 | } 174 | 175 | func (parser *Parser) consumeInteger() (value int, err error) { 176 | originalOffset := parser.offset 177 | defer func() { 178 | if err != nil { 179 | parser.offset = originalOffset 180 | } 181 | }() 182 | 183 | var token Token 184 | token, err = parser.consumeToken(TokenKindNumber) 185 | if err != nil { 186 | return 187 | } 188 | 189 | value, err = strconv.Atoi(token.Value) 190 | if err != nil { 191 | return 192 | } 193 | 194 | return 195 | } 196 | 197 | func (parser *Parser) consumeNumberType() (precision int, err error) { 198 | originalOffset := parser.offset 199 | defer func() { 200 | if err != nil { 201 | parser.offset = originalOffset 202 | } 203 | }() 204 | 205 | _, err = parser.consumeSpecificWord(VariableTypeNumber) 206 | if err != nil { 207 | return 208 | } 209 | 210 | _, err = parser.consumeSpecificWord("with") 211 | if err != nil { 212 | // That's OK, we can safely bail out here. 213 | precision = DefaultNumericPrecision 214 | err = nil 215 | return 216 | } 217 | 218 | precision, err = parser.consumeInteger() 219 | if err != nil { 220 | return 221 | } 222 | 223 | _, err = parser.consumeSpecificWord("decimal") 224 | if err != nil { 225 | return 226 | } 227 | 228 | _, err = parser.consumeSpecificWord("places") 229 | if err != nil { 230 | // Even through "places" or "place" is allowed for any number of decimal 231 | // places, it reads better to say "1 decimal place". 232 | _, err = parser.consumeSpecificWord("place") 233 | if err != nil { 234 | return 235 | } 236 | 237 | err = nil 238 | } 239 | 240 | return 241 | } 242 | 243 | func (parser *Parser) consumeType() (ty string, precision int, err error) { 244 | originalOffset := parser.offset 245 | defer func() { 246 | if err != nil { 247 | parser.offset = originalOffset 248 | } 249 | }() 250 | 251 | // The "a" and "an" are optional so that it is easier to read in some cases: 252 | // 253 | // is text 254 | // is a number 255 | // is an order receipt 256 | 257 | _, err = parser.consumeSpecificWord("a") 258 | if err == nil { 259 | goto consumeType 260 | } 261 | 262 | _, err = parser.consumeSpecificWord("an") 263 | if err == nil { 264 | goto consumeType 265 | } 266 | 267 | consumeType: 268 | precision, err = parser.consumeNumberType() 269 | if err == nil { 270 | return "number", precision, err 271 | } 272 | 273 | ty, err = parser.consumeWord() 274 | if err == nil { 275 | return ty, 0, nil 276 | } 277 | 278 | return "", 0, fmt.Errorf("expected variable type, but got %s", ty) 279 | } 280 | 281 | // Examples: 282 | // 283 | // some-variable is text 284 | // some-variable is number 285 | // some-variable is number with 2 decimal places 286 | // 287 | func (parser *Parser) consumeVariableIsType() (definition *VariableDefinition, err error) { 288 | originalOffset := parser.offset 289 | defer func() { 290 | if err != nil { 291 | parser.offset = originalOffset 292 | } 293 | }() 294 | 295 | definition = new(VariableDefinition) 296 | 297 | definition.Name, err = parser.consumeWord() 298 | if err != nil { 299 | return nil, err 300 | } 301 | 302 | _, err = parser.consumeSpecificWord("is") 303 | if err != nil { 304 | return nil, err 305 | } 306 | 307 | definition.Type, definition.Precision, err = parser.consumeType() 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | return 313 | } 314 | 315 | // foo is text, bar is text 316 | func (parser *Parser) consumeVariableIsTypeList() (list map[string]*VariableDefinition, err error) { 317 | originalOffset := parser.offset 318 | defer func() { 319 | if err != nil { 320 | parser.offset = originalOffset 321 | } 322 | }() 323 | 324 | list = map[string]*VariableDefinition{} 325 | 326 | for !parser.isFinished() { 327 | definition, err := parser.consumeVariableIsType() 328 | if err != nil { 329 | return nil, err 330 | } 331 | 332 | list[definition.Name] = definition 333 | 334 | err = parser.consumeComma() 335 | if err != nil { 336 | break 337 | } 338 | } 339 | 340 | return 341 | } 342 | 343 | func (parser *Parser) consumeSentence(varMap map[string]*VariableDefinition) (sentence *Sentence, err error) { 344 | originalOffset := parser.offset 345 | defer func() { 346 | if err != nil { 347 | parser.offset = originalOffset 348 | } 349 | }() 350 | 351 | sentence = new(Sentence) 352 | 353 | for !parser.isFinished() { 354 | word, err := parser.consumeSentenceWord(varMap) 355 | if err != nil { 356 | break 357 | } 358 | 359 | sentence.Words = append(sentence.Words, word) 360 | } 361 | 362 | return 363 | } 364 | 365 | func (parser *Parser) consumeQuestionAnswer() (answer *QuestionAnswer, err error) { 366 | originalOffset := parser.offset 367 | defer func() { 368 | if err != nil { 369 | parser.offset = originalOffset 370 | } 371 | }() 372 | 373 | word, err := parser.consumeSpecificWord("yes", "no") 374 | if err != nil { 375 | return nil, err 376 | } 377 | 378 | return &QuestionAnswer{ 379 | Yes: word == "yes", 380 | }, nil 381 | } 382 | 383 | func (parser *Parser) consumeQuestionAnswerCall() (answer *QuestionAnswer, err error) { 384 | originalOffset := parser.offset 385 | defer func() { 386 | if err != nil { 387 | parser.offset = originalOffset 388 | } 389 | }() 390 | 391 | answer, err = parser.consumeQuestionAnswer() 392 | if err != nil { 393 | return nil, err 394 | } 395 | 396 | _, err = parser.consumeToken(TokenKindEndOfLine) 397 | if err != nil { 398 | return nil, err 399 | } 400 | 401 | return answer, nil 402 | } 403 | 404 | func (parser *Parser) consumeSentenceCall(varMap map[string]*VariableDefinition) (sentence *Sentence, err error) { 405 | originalOffset := parser.offset 406 | defer func() { 407 | if err != nil { 408 | parser.offset = originalOffset 409 | } 410 | }() 411 | 412 | sentence, err = parser.consumeSentence(varMap) 413 | if err != nil { 414 | return 415 | } 416 | 417 | _, err = parser.consumeToken(TokenKindEndOfLine) 418 | if err != nil { 419 | return 420 | } 421 | 422 | return 423 | } 424 | 425 | func (parser *Parser) consumeSentenceOrAnswer(varMap map[string]*VariableDefinition) (_ Statement, err error) { 426 | originalOffset := parser.offset 427 | defer func() { 428 | if err != nil { 429 | parser.offset = originalOffset 430 | } 431 | }() 432 | 433 | answer, err := parser.consumeQuestionAnswer() 434 | if err == nil { 435 | return answer, nil 436 | } 437 | 438 | return parser.consumeSentence(varMap) 439 | } 440 | 441 | func (parser *Parser) consumeSentenceCallOrAnswerCall(varMap map[string]*VariableDefinition) (_ Statement, err error) { 442 | originalOffset := parser.offset 443 | defer func() { 444 | if err != nil { 445 | parser.offset = originalOffset 446 | } 447 | }() 448 | 449 | answer, err := parser.consumeQuestionAnswerCall() 450 | if err == nil { 451 | return answer, nil 452 | } 453 | 454 | return parser.consumeSentenceCall(varMap) 455 | } 456 | 457 | func (parser *Parser) consumeFunction() (function *Function, err error) { 458 | originalOffset := parser.offset 459 | defer func() { 460 | if err != nil { 461 | parser.offset = originalOffset 462 | } 463 | }() 464 | 465 | function, err = parser.consumeFunctionDeclaration() 466 | if err != nil { 467 | return nil, err 468 | } 469 | 470 | for !parser.isFinished() { 471 | // declare ... 472 | definition, err := parser.consumeDeclare() 473 | if err == nil { 474 | definition.LocalScope = true 475 | function.AppendVariable(definition) 476 | continue 477 | } 478 | 479 | // if/unless ... 480 | ifStmt, err := parser.consumeIf(function.VariableMap()) 481 | if err == nil { 482 | function.AppendStatement(ifStmt) 483 | continue 484 | } 485 | 486 | // while/until ... 487 | whileStmt, err := parser.consumeWhile(function.VariableMap()) 488 | if err == nil { 489 | function.AppendStatement(whileStmt) 490 | continue 491 | } 492 | 493 | // TODO: yes/no cannot be used outside of questions 494 | sentenceOrAnswer, err := 495 | parser.consumeSentenceCallOrAnswerCall(function.VariableMap()) 496 | if err == nil { 497 | function.AppendStatement(sentenceOrAnswer) 498 | continue 499 | } 500 | 501 | return function, nil 502 | } 503 | 504 | return function, nil 505 | } 506 | 507 | func (parser *Parser) consumeFunctionDeclaration() (function *Function, err error) { 508 | originalOffset := parser.offset 509 | defer func() { 510 | if err != nil { 511 | parser.offset = originalOffset 512 | } 513 | }() 514 | 515 | var sentence *Sentence 516 | sentence, err = parser.consumeSentence(nil) 517 | if err != nil { 518 | return 519 | } 520 | 521 | function = &Function{ 522 | Definition: sentence, 523 | } 524 | 525 | _, err = parser.consumeToken(TokenKindOpenBracket) 526 | if err == nil { 527 | vars, err := parser.consumeVariableIsTypeList() 528 | if err != nil { 529 | return nil, err 530 | } 531 | 532 | _, err = parser.consumeToken(TokenKindCloseBracket) 533 | if err != nil { 534 | return nil, err 535 | } 536 | 537 | for i, word := range function.Definition.Words { 538 | if ty, ok := vars[word.(string)]; ok { 539 | function.Definition.Words[i] = VariableReference(word.(string)) 540 | 541 | // Note: It's important that we add the arguments in the order 542 | // that they appear rather than the order that they are defined. 543 | // Appending them in this loop will ensure that. 544 | function.AppendArgument(word.(string), ty.Type) 545 | } 546 | } 547 | } 548 | 549 | _, err = parser.consumeToken(TokenKindQuestion) 550 | if err == nil { 551 | function.IsQuestion = true 552 | } else { 553 | _, err = parser.consumeToken(TokenKindColon) 554 | if err != nil { 555 | return nil, err 556 | } 557 | } 558 | 559 | parser.consumeTokens([]string{TokenKindEndOfLine}) 560 | 561 | return function, nil 562 | } 563 | 564 | func (parser *Parser) isFinished() bool { 565 | return parser.tokens[parser.offset].Kind == TokenKindEndOfFile 566 | } 567 | 568 | func (parser *Parser) lineIsFinished() bool { 569 | return parser.tokens[parser.offset].Kind == TokenKindEndOfLine 570 | } 571 | 572 | func (parser *Parser) consumeDeclare() (definition *VariableDefinition, err error) { 573 | // TODO: Can these be made easier? 574 | originalOffset := parser.offset 575 | defer func() { 576 | if err != nil { 577 | parser.offset = originalOffset 578 | } 579 | }() 580 | 581 | _, err = parser.consumeSpecificWord(WordDeclare) 582 | if err != nil { 583 | return 584 | } 585 | 586 | definition, err = parser.consumeVariableIsType() 587 | if err != nil { 588 | return 589 | } 590 | 591 | _, err = parser.consumeToken(TokenKindEndOfLine) 592 | if err != nil { 593 | return 594 | } 595 | 596 | return 597 | } 598 | 599 | func (parser *Parser) consumeIf(varMap map[string]*VariableDefinition) (ifStmt *If, err error) { 600 | originalOffset := parser.offset 601 | defer func() { 602 | if err != nil { 603 | parser.offset = originalOffset 604 | } 605 | }() 606 | 607 | ifStmt = &If{} 608 | 609 | _, err = parser.consumeSpecificWord(WordIf) 610 | if err != nil { 611 | _, err = parser.consumeSpecificWord(WordUnless) 612 | if err != nil { 613 | return nil, errors.New("expected if or unless") 614 | } 615 | 616 | ifStmt.Unless = true 617 | } 618 | 619 | // TODO: If we hit and if, we must not allow it to process the line as a 620 | // sentence. 621 | 622 | ifStmt.Condition, err = parser.consumeCondition(varMap) 623 | if err != nil { 624 | // It must be a question instead of a condition. 625 | ifStmt.Question, err = parser.consumeSentence(varMap) 626 | 627 | if err != nil { 628 | return 629 | } 630 | } 631 | 632 | err = parser.consumeComma() 633 | if err != nil { 634 | return 635 | } 636 | 637 | ifStmt.True, err = parser.consumeSentenceOrAnswer(varMap) 638 | if err != nil { 639 | return 640 | } 641 | 642 | // Bail out if safely if there is no "otherwise". 643 | _, err = parser.consumeToken(TokenKindEndOfLine) 644 | if err == nil { 645 | return 646 | } 647 | 648 | err = parser.consumeComma() 649 | if err != nil { 650 | return 651 | } 652 | 653 | _, err = parser.consumeSpecificWord(WordOtherwise) 654 | if err != nil { 655 | return 656 | } 657 | 658 | ifStmt.False, err = parser.consumeSentenceOrAnswer(varMap) 659 | if err != nil { 660 | return 661 | } 662 | 663 | _, err = parser.consumeToken(TokenKindEndOfLine) 664 | if err != nil { 665 | return 666 | } 667 | 668 | return 669 | } 670 | 671 | func (parser *Parser) consumeCondition(varMap map[string]*VariableDefinition) (condition *Condition, err error) { 672 | originalOffset := parser.offset 673 | defer func() { 674 | if err != nil { 675 | parser.offset = originalOffset 676 | } 677 | }() 678 | 679 | condition = &Condition{} 680 | 681 | condition.Left, err = parser.consumeSentenceWord(varMap) 682 | if err != nil { 683 | return nil, err 684 | } 685 | 686 | condition.Operator, err = parser.consumeOperator() 687 | if err != nil { 688 | return nil, err 689 | } 690 | 691 | condition.Right, err = parser.consumeSentenceWord(varMap) 692 | if err != nil { 693 | return nil, err 694 | } 695 | 696 | return condition, nil 697 | } 698 | 699 | func (parser *Parser) consumeOperator() (string, error) { 700 | operatorToken, err := parser.consumeToken(TokenKindOperator) 701 | if err != nil { 702 | return "", err 703 | } 704 | 705 | return operatorToken.Value, nil 706 | } 707 | 708 | func (parser *Parser) consumeComma() error { 709 | _, err := parser.consumeToken(TokenKindComma) 710 | if err != nil { 711 | return err 712 | } 713 | 714 | // Ignore the error as the new line is optional. 715 | _, _ = parser.consumeToken(TokenKindEndOfLine) 716 | 717 | return nil 718 | } 719 | 720 | func (parser *Parser) consumeWhile(varMap map[string]*VariableDefinition) (whileStmt *While, err error) { 721 | originalOffset := parser.offset 722 | defer func() { 723 | if err != nil { 724 | parser.offset = originalOffset 725 | } 726 | }() 727 | 728 | whileStmt = &While{} 729 | 730 | _, err = parser.consumeSpecificWord(WordWhile) 731 | if err != nil { 732 | _, err = parser.consumeSpecificWord(WordUntil) 733 | if err != nil { 734 | return nil, errors.New("expected while or until") 735 | } 736 | 737 | whileStmt.Until = true 738 | } 739 | 740 | // TODO: If we hit a "while", we must not allow it to process the line as a 741 | // sentence. 742 | 743 | whileStmt.Condition, err = parser.consumeCondition(varMap) 744 | if err != nil { 745 | // It must be a question instead of a condition. 746 | whileStmt.Question, err = parser.consumeSentence(varMap) 747 | 748 | if err != nil { 749 | return 750 | } 751 | } 752 | 753 | err = parser.consumeComma() 754 | if err != nil { 755 | return 756 | } 757 | 758 | whileStmt.True, err = parser.consumeSentence(varMap) 759 | if err != nil { 760 | return 761 | } 762 | 763 | _, err = parser.consumeToken(TokenKindEndOfLine) 764 | if err != nil { 765 | return 766 | } 767 | 768 | return 769 | } 770 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | "github.com/google/go-cmp/cmp/cmpopts" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var parserTests = map[string]struct { 13 | bento string 14 | expected *Program 15 | }{ 16 | "Empty": { 17 | bento: "", 18 | expected: &Program{ 19 | Functions: map[string]*Function{}, 20 | }, 21 | }, 22 | "EmptyStart": { 23 | bento: "start:", 24 | expected: &Program{ 25 | Functions: map[string]*Function{ 26 | "start": { 27 | Definition: &Sentence{Words: []interface{}{"start"}}, 28 | }, 29 | }, 30 | }, 31 | }, 32 | "Display": { 33 | bento: `start: Display "Hello, World!"`, 34 | expected: &Program{ 35 | Functions: map[string]*Function{ 36 | "start": { 37 | Definition: &Sentence{Words: []interface{}{"start"}}, 38 | Statements: []Statement{ 39 | &Sentence{ 40 | Words: []interface{}{ 41 | "display", NewText("Hello, World!"), 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | "Display2": { 50 | bento: "start:\nDisplay \"Hello, World!\"", 51 | expected: &Program{ 52 | Functions: map[string]*Function{ 53 | "start": { 54 | Definition: &Sentence{Words: []interface{}{"start"}}, 55 | Statements: []Statement{ 56 | &Sentence{ 57 | Words: []interface{}{ 58 | "display", NewText("Hello, World!"), 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | "DisplayTwice": { 67 | bento: "start: Display \"hello\"\ndisplay \"twice!\"", 68 | expected: &Program{ 69 | Functions: map[string]*Function{ 70 | "start": { 71 | Definition: &Sentence{Words: []interface{}{"start"}}, 72 | Statements: []Statement{ 73 | &Sentence{ 74 | Words: []interface{}{ 75 | "display", NewText("hello"), 76 | }, 77 | }, 78 | &Sentence{ 79 | Words: []interface{}{ 80 | "display", NewText("twice!"), 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | "Declare1": { 89 | bento: "start: declare some-variable is text", 90 | expected: &Program{ 91 | Functions: map[string]*Function{ 92 | "start": { 93 | Definition: &Sentence{Words: []interface{}{"start"}}, 94 | Variables: []*VariableDefinition{ 95 | { 96 | Name: "some-variable", 97 | Type: "text", 98 | LocalScope: true, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | "Declare2": { 106 | bento: "start: declare foo is text\ndisplay foo", 107 | expected: &Program{ 108 | Functions: map[string]*Function{ 109 | "start": { 110 | Definition: &Sentence{Words: []interface{}{"start"}}, 111 | Variables: []*VariableDefinition{ 112 | { 113 | Name: "foo", 114 | Type: "text", 115 | LocalScope: true, 116 | }, 117 | }, 118 | Statements: []Statement{ 119 | &Sentence{ 120 | Words: []interface{}{ 121 | "display", VariableReference("foo"), 122 | }, 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | }, 129 | "Function1": { 130 | bento: "start: display \"hi\"\ndo something:\ndisplay \"ok\"", 131 | expected: &Program{ 132 | Functions: map[string]*Function{ 133 | "start": { 134 | Definition: &Sentence{Words: []interface{}{"start"}}, 135 | Statements: []Statement{ 136 | &Sentence{ 137 | Words: []interface{}{ 138 | "display", NewText("hi"), 139 | }, 140 | }, 141 | }, 142 | }, 143 | "do something": { 144 | Definition: &Sentence{Words: []interface{}{"do", "something"}}, 145 | Statements: []Statement{ 146 | &Sentence{ 147 | Words: []interface{}{ 148 | "display", NewText("ok"), 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | }, 155 | }, 156 | "Function2": { 157 | bento: "start:do something\ndo something:\ndisplay \"ok\"", 158 | expected: &Program{ 159 | Functions: map[string]*Function{ 160 | "start": { 161 | Definition: &Sentence{Words: []interface{}{"start"}}, 162 | Statements: []Statement{ 163 | &Sentence{ 164 | Words: []interface{}{ 165 | "do", "something", 166 | }, 167 | }, 168 | }, 169 | }, 170 | "do something": { 171 | Definition: &Sentence{Words: []interface{}{"do", "something"}}, 172 | Statements: []Statement{ 173 | &Sentence{ 174 | Words: []interface{}{ 175 | "display", NewText("ok"), 176 | }, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }, 182 | }, 183 | "FunctionWithArgument": { 184 | bento: "greet persons-name now (persons-name is text):", 185 | expected: &Program{ 186 | Functions: map[string]*Function{ 187 | "greet ? now": { 188 | Definition: &Sentence{Words: []interface{}{"greet", VariableReference("persons-name"), "now"}}, 189 | Variables: []*VariableDefinition{ 190 | { 191 | Name: "persons-name", 192 | Type: "text", 193 | LocalScope: false, 194 | }, 195 | }, 196 | }, 197 | }, 198 | }, 199 | }, 200 | "CallWithArgument": { 201 | bento: "greet persons-name now (persons-name is text):\ndisplay persons-name", 202 | expected: &Program{ 203 | Functions: map[string]*Function{ 204 | "greet ? now": { 205 | Definition: &Sentence{Words: []interface{}{"greet", VariableReference("persons-name"), "now"}}, 206 | Variables: []*VariableDefinition{ 207 | { 208 | Name: "persons-name", 209 | Type: "text", 210 | LocalScope: false, 211 | }, 212 | }, 213 | Statements: []Statement{ 214 | &Sentence{ 215 | Words: []interface{}{ 216 | "display", VariableReference("persons-name"), 217 | }, 218 | }, 219 | }, 220 | }, 221 | }, 222 | }, 223 | }, 224 | "FunctionWithArguments": { 225 | bento: "say greeting to persons-name (persons-name is text, greeting is text):", 226 | expected: &Program{ 227 | Functions: map[string]*Function{ 228 | "say ? to ?": { 229 | Definition: &Sentence{Words: []interface{}{ 230 | "say", 231 | VariableReference("greeting"), 232 | "to", 233 | VariableReference("persons-name"), 234 | }}, 235 | Variables: []*VariableDefinition{ 236 | { 237 | Name: "greeting", 238 | Type: "text", 239 | LocalScope: false, 240 | }, 241 | { 242 | Name: "persons-name", 243 | Type: "text", 244 | LocalScope: false, 245 | }, 246 | }, 247 | }, 248 | }, 249 | }, 250 | }, 251 | "DeclareNumber": { 252 | bento: "start: declare foo is number", 253 | expected: &Program{ 254 | Functions: map[string]*Function{ 255 | "start": { 256 | Definition: &Sentence{Words: []interface{}{"start"}}, 257 | Variables: []*VariableDefinition{ 258 | { 259 | Name: "foo", 260 | Type: "number", 261 | LocalScope: true, 262 | Precision: 6, 263 | }, 264 | }, 265 | }, 266 | }, 267 | }, 268 | }, 269 | "SetNegativeNumber": { 270 | bento: "start: declare foo is number\nset foo to -1.23", 271 | expected: &Program{ 272 | Functions: map[string]*Function{ 273 | "start": { 274 | Definition: &Sentence{Words: []interface{}{"start"}}, 275 | Variables: []*VariableDefinition{ 276 | { 277 | Name: "foo", 278 | Type: "number", 279 | LocalScope: true, 280 | Precision: 6, 281 | }, 282 | }, 283 | Statements: []Statement{ 284 | &Sentence{ 285 | Words: []interface{}{ 286 | "set", VariableReference("foo"), "to", NewNumber("-1.23", 6), 287 | }, 288 | }, 289 | }, 290 | }, 291 | }, 292 | }, 293 | }, 294 | "InlineIf": { 295 | bento: "start: declare foo is text\nif foo = \"qux\", quux 1.234\ncorge", 296 | expected: &Program{ 297 | Functions: map[string]*Function{ 298 | "start": { 299 | Definition: &Sentence{Words: []interface{}{"start"}}, 300 | Variables: []*VariableDefinition{ 301 | { 302 | Name: "foo", 303 | Type: "text", 304 | LocalScope: true, 305 | }, 306 | }, 307 | Statements: []Statement{ 308 | &If{ 309 | Condition: &Condition{ 310 | Left: VariableReference("foo"), 311 | Right: NewText("qux"), 312 | Operator: OperatorEqual, 313 | }, 314 | True: &Sentence{ 315 | Words: []interface{}{ 316 | "quux", NewNumber("1.234", 6), 317 | }, 318 | }, 319 | }, 320 | &Sentence{ 321 | Words: []interface{}{ 322 | "corge", 323 | }, 324 | }, 325 | }, 326 | }, 327 | }, 328 | }, 329 | }, 330 | "InlineIfElse": { 331 | bento: "start: declare foo is text\nif foo = \"qux\", quux 1.234, otherwise corge\ndisplay", 332 | expected: &Program{ 333 | Functions: map[string]*Function{ 334 | "start": { 335 | Definition: &Sentence{Words: []interface{}{"start"}}, 336 | Variables: []*VariableDefinition{ 337 | { 338 | Name: "foo", 339 | Type: "text", 340 | LocalScope: true, 341 | }, 342 | }, 343 | Statements: []Statement{ 344 | &If{ 345 | Condition: &Condition{ 346 | Left: VariableReference("foo"), 347 | Right: NewText("qux"), 348 | Operator: OperatorEqual, 349 | }, 350 | True: &Sentence{ 351 | Words: []interface{}{ 352 | "quux", NewNumber("1.234", 6), 353 | }, 354 | }, 355 | False: &Sentence{ 356 | Words: []interface{}{ 357 | "corge", 358 | }, 359 | }, 360 | }, 361 | &Sentence{ 362 | Words: []interface{}{ 363 | "display", 364 | }, 365 | }, 366 | }, 367 | }, 368 | }, 369 | }, 370 | }, 371 | "InlineUnless": { 372 | bento: "start: declare foo is text\nunless foo = \"qux\", quux 1.234\ncorge", 373 | expected: &Program{ 374 | Functions: map[string]*Function{ 375 | "start": { 376 | Definition: &Sentence{Words: []interface{}{"start"}}, 377 | Variables: []*VariableDefinition{ 378 | { 379 | Name: "foo", 380 | Type: "text", 381 | LocalScope: true, 382 | }, 383 | }, 384 | Statements: []Statement{ 385 | &If{ 386 | Unless: true, 387 | Condition: &Condition{ 388 | Left: VariableReference("foo"), 389 | Right: NewText("qux"), 390 | Operator: OperatorEqual, 391 | }, 392 | True: &Sentence{ 393 | Words: []interface{}{ 394 | "quux", NewNumber("1.234", 6), 395 | }, 396 | }, 397 | }, 398 | &Sentence{ 399 | Words: []interface{}{ 400 | "corge", 401 | }, 402 | }, 403 | }, 404 | }, 405 | }, 406 | }, 407 | }, 408 | "InlineUnlessElse": { 409 | bento: "start: declare foo is text\nunless foo = \"qux\", quux 1.234, otherwise corge\ndisplay", 410 | expected: &Program{ 411 | Functions: map[string]*Function{ 412 | "start": { 413 | Definition: &Sentence{Words: []interface{}{"start"}}, 414 | Variables: []*VariableDefinition{ 415 | { 416 | Name: "foo", 417 | Type: "text", 418 | LocalScope: true, 419 | }, 420 | }, 421 | Statements: []Statement{ 422 | &If{ 423 | Unless: true, 424 | Condition: &Condition{ 425 | Left: VariableReference("foo"), 426 | Right: NewText("qux"), 427 | Operator: OperatorEqual, 428 | }, 429 | True: &Sentence{ 430 | Words: []interface{}{ 431 | "quux", NewNumber("1.234", 6), 432 | }, 433 | }, 434 | False: &Sentence{ 435 | Words: []interface{}{ 436 | "corge", 437 | }, 438 | }, 439 | }, 440 | &Sentence{ 441 | Words: []interface{}{ 442 | "display", 443 | }, 444 | }, 445 | }, 446 | }, 447 | }, 448 | }, 449 | }, 450 | "InlineWhile": { 451 | bento: "start: declare foo is text\nwhile foo = \"qux\", quux 1.234\ncorge", 452 | expected: &Program{ 453 | Functions: map[string]*Function{ 454 | "start": { 455 | Definition: &Sentence{Words: []interface{}{"start"}}, 456 | Variables: []*VariableDefinition{ 457 | { 458 | Name: "foo", 459 | Type: "text", 460 | LocalScope: true, 461 | }, 462 | }, 463 | Statements: []Statement{ 464 | &While{ 465 | Condition: &Condition{ 466 | Left: VariableReference("foo"), 467 | Right: NewText("qux"), 468 | Operator: OperatorEqual, 469 | }, 470 | True: &Sentence{ 471 | Words: []interface{}{ 472 | "quux", NewNumber("1.234", 6), 473 | }, 474 | }, 475 | }, 476 | &Sentence{ 477 | Words: []interface{}{ 478 | "corge", 479 | }, 480 | }, 481 | }, 482 | }, 483 | }, 484 | }, 485 | }, 486 | "InlineUntil": { 487 | bento: "start: declare foo is text\nuntil foo = \"qux\", quux 1.234\ncorge", 488 | expected: &Program{ 489 | Functions: map[string]*Function{ 490 | "start": { 491 | Definition: &Sentence{Words: []interface{}{"start"}}, 492 | Variables: []*VariableDefinition{ 493 | { 494 | Name: "foo", 495 | Type: "text", 496 | LocalScope: true, 497 | }, 498 | }, 499 | Statements: []Statement{ 500 | &While{ 501 | Until: true, 502 | Condition: &Condition{ 503 | Left: VariableReference("foo"), 504 | Right: NewText("qux"), 505 | Operator: OperatorEqual, 506 | }, 507 | True: &Sentence{ 508 | Words: []interface{}{ 509 | "quux", NewNumber("1.234", 6), 510 | }, 511 | }, 512 | }, 513 | &Sentence{ 514 | Words: []interface{}{ 515 | "corge", 516 | }, 517 | }, 518 | }, 519 | }, 520 | }, 521 | }, 522 | }, 523 | "NumberWithPrecision": { 524 | bento: "start: declare foo is number with 2 decimal places\ndisplay foo", 525 | expected: &Program{ 526 | Functions: map[string]*Function{ 527 | "start": { 528 | Definition: &Sentence{Words: []interface{}{"start"}}, 529 | Variables: []*VariableDefinition{ 530 | { 531 | Name: "foo", 532 | Type: "number", 533 | LocalScope: true, 534 | Precision: 2, 535 | }, 536 | }, 537 | Statements: []Statement{ 538 | &Sentence{ 539 | Words: []interface{}{ 540 | "display", VariableReference("foo"), 541 | }, 542 | }, 543 | }, 544 | }, 545 | }, 546 | }, 547 | }, 548 | "NumberWithPrecision1": { 549 | bento: "start: declare foo is number with 1 decimal place\ndisplay foo", 550 | expected: &Program{ 551 | Functions: map[string]*Function{ 552 | "start": { 553 | Definition: &Sentence{Words: []interface{}{"start"}}, 554 | Variables: []*VariableDefinition{ 555 | { 556 | Name: "foo", 557 | Type: "number", 558 | LocalScope: true, 559 | Precision: 1, 560 | }, 561 | }, 562 | Statements: []Statement{ 563 | &Sentence{ 564 | Words: []interface{}{ 565 | "display", VariableReference("foo"), 566 | }, 567 | }, 568 | }, 569 | }, 570 | }, 571 | }, 572 | }, 573 | "DeclareUsingA": { 574 | bento: "start: declare some-variable is a number", 575 | expected: &Program{ 576 | Functions: map[string]*Function{ 577 | "start": { 578 | Definition: &Sentence{Words: []interface{}{"start"}}, 579 | Variables: []*VariableDefinition{ 580 | { 581 | Name: "some-variable", 582 | Type: "number", 583 | LocalScope: true, 584 | Precision: 6, 585 | }, 586 | }, 587 | }, 588 | }, 589 | }, 590 | }, 591 | "DeclareUsingAn": { 592 | bento: "start: declare some-variable is an number", 593 | expected: &Program{ 594 | Functions: map[string]*Function{ 595 | "start": { 596 | Definition: &Sentence{Words: []interface{}{"start"}}, 597 | Variables: []*VariableDefinition{ 598 | { 599 | Name: "some-variable", 600 | Type: "number", 601 | LocalScope: true, 602 | Precision: 6, 603 | }, 604 | }, 605 | }, 606 | }, 607 | }, 608 | }, 609 | "Multiline1": { 610 | bento: "start: declare some-variable...\n is an number", 611 | expected: &Program{ 612 | Functions: map[string]*Function{ 613 | "start": { 614 | Definition: &Sentence{Words: []interface{}{"start"}}, 615 | Variables: []*VariableDefinition{ 616 | { 617 | Name: "some-variable", 618 | Type: "number", 619 | LocalScope: true, 620 | Precision: 6, 621 | }, 622 | }, 623 | }, 624 | }, 625 | }, 626 | }, 627 | "Multiline2": { 628 | bento: "start: declare some-variable\t ...\n is...\n an number", 629 | expected: &Program{ 630 | Functions: map[string]*Function{ 631 | "start": { 632 | Definition: &Sentence{Words: []interface{}{"start"}}, 633 | Variables: []*VariableDefinition{ 634 | { 635 | Name: "some-variable", 636 | Type: "number", 637 | LocalScope: true, 638 | Precision: 6, 639 | }, 640 | }, 641 | }, 642 | }, 643 | }, 644 | }, 645 | "BlackholeVariable": { 646 | bento: "start: display _", 647 | expected: &Program{ 648 | Functions: map[string]*Function{ 649 | "start": { 650 | Definition: &Sentence{Words: []interface{}{"start"}}, 651 | Statements: []Statement{ 652 | &Sentence{ 653 | Words: []interface{}{ 654 | "display", VariableReference("_"), 655 | }, 656 | }, 657 | }, 658 | }, 659 | }, 660 | }, 661 | }, 662 | "WordStartingWithUnderscore": { 663 | bento: "start: display _foo", 664 | expected: &Program{ 665 | Functions: map[string]*Function{ 666 | "start": { 667 | Definition: &Sentence{Words: []interface{}{"start"}}, 668 | Statements: []Statement{ 669 | &Sentence{ 670 | Words: []interface{}{ 671 | "display", "_foo", 672 | }, 673 | }, 674 | }, 675 | }, 676 | }, 677 | }, 678 | }, 679 | "QuestionDefinition1Yes": { 680 | bento: "is good?\nyes", 681 | expected: &Program{ 682 | Functions: map[string]*Function{ 683 | "is good": { 684 | IsQuestion: true, 685 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 686 | Statements: []Statement{ 687 | &QuestionAnswer{ 688 | Yes: true, 689 | }, 690 | }, 691 | }, 692 | }, 693 | }, 694 | }, 695 | "QuestionDefinition1No": { 696 | bento: "is good?\nno", 697 | expected: &Program{ 698 | Functions: map[string]*Function{ 699 | "is good": { 700 | IsQuestion: true, 701 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 702 | Statements: []Statement{ 703 | &QuestionAnswer{ 704 | Yes: false, 705 | }, 706 | }, 707 | }, 708 | }, 709 | }, 710 | }, 711 | "QuestionDefinition2YesAndOtherWords": { 712 | bento: "is good?\nyes then", 713 | expected: &Program{ 714 | Functions: map[string]*Function{ 715 | "is good": { 716 | IsQuestion: true, 717 | Definition: &Sentence{Words: []interface{}{"is", "good"}}, 718 | Statements: []Statement{ 719 | &Sentence{ 720 | Words: []interface{}{ 721 | "yes", "then", 722 | }, 723 | }, 724 | }, 725 | }, 726 | }, 727 | }, 728 | }, 729 | "QuestionDefinitionWithVars": { 730 | bento: "foo is good (foo is text)?\nyes", 731 | expected: &Program{ 732 | Functions: map[string]*Function{ 733 | "? is good": { 734 | IsQuestion: true, 735 | Definition: &Sentence{Words: []interface{}{ 736 | VariableReference("foo"), "is", "good"}, 737 | }, 738 | Variables: []*VariableDefinition{ 739 | { 740 | Name: "foo", 741 | Type: "text", 742 | }, 743 | }, 744 | Statements: []Statement{ 745 | &QuestionAnswer{ 746 | Yes: true, 747 | }, 748 | }, 749 | }, 750 | }, 751 | }, 752 | }, 753 | "IfQuestion": { 754 | bento: "start: if something is true, all good", 755 | expected: &Program{ 756 | Functions: map[string]*Function{ 757 | "start": { 758 | Definition: &Sentence{Words: []interface{}{"start"}}, 759 | Statements: []Statement{ 760 | &If{ 761 | Question: &Sentence{ 762 | Words: []interface{}{"something", "is", "true"}, 763 | }, 764 | True: &Sentence{ 765 | Words: []interface{}{"all", "good"}, 766 | }, 767 | }, 768 | }, 769 | }, 770 | }, 771 | }, 772 | }, 773 | "WhileQuestion": { 774 | bento: "start: while something is true, all good", 775 | expected: &Program{ 776 | Functions: map[string]*Function{ 777 | "start": { 778 | Definition: &Sentence{Words: []interface{}{"start"}}, 779 | Statements: []Statement{ 780 | &While{ 781 | Question: &Sentence{ 782 | Words: []interface{}{"something", "is", "true"}, 783 | }, 784 | True: &Sentence{ 785 | Words: []interface{}{"all", "good"}, 786 | }, 787 | }, 788 | }, 789 | }, 790 | }, 791 | }, 792 | }, 793 | "IfYes": { 794 | bento: "start? if something, yes", 795 | expected: &Program{ 796 | Functions: map[string]*Function{ 797 | "start": { 798 | IsQuestion: true, 799 | Definition: &Sentence{Words: []interface{}{"start"}}, 800 | Statements: []Statement{ 801 | &If{ 802 | Question: &Sentence{ 803 | Words: []interface{}{"something"}, 804 | }, 805 | True: &QuestionAnswer{ 806 | Yes: true, 807 | }, 808 | }, 809 | }, 810 | }, 811 | }, 812 | }, 813 | }, 814 | "IfYesNo": { 815 | bento: "start? if something, no, otherwise yes", 816 | expected: &Program{ 817 | Functions: map[string]*Function{ 818 | "start": { 819 | IsQuestion: true, 820 | Definition: &Sentence{Words: []interface{}{"start"}}, 821 | Statements: []Statement{ 822 | &If{ 823 | Question: &Sentence{ 824 | Words: []interface{}{"something"}, 825 | }, 826 | True: &QuestionAnswer{ 827 | Yes: false, 828 | }, 829 | False: &QuestionAnswer{ 830 | Yes: true, 831 | }, 832 | }, 833 | }, 834 | }, 835 | }, 836 | }, 837 | }, 838 | "MultilineIf": { 839 | bento: "start:\nif something,\n next line\ndisplay hi", 840 | expected: &Program{ 841 | Functions: map[string]*Function{ 842 | "start": { 843 | Definition: &Sentence{Words: []interface{}{"start"}}, 844 | Statements: []Statement{ 845 | &If{ 846 | Question: &Sentence{ 847 | Words: []interface{}{"something"}, 848 | }, 849 | True: &Sentence{ 850 | Words: []interface{}{"next", "line"}, 851 | }, 852 | }, 853 | &Sentence{ 854 | Words: []interface{}{"display", "hi"}, 855 | }, 856 | }, 857 | }, 858 | }, 859 | }, 860 | }, 861 | "MultilineIf2": { 862 | bento: "start:\nif something,\n next line,\n otherwise foo\ndisplay hi", 863 | expected: &Program{ 864 | Functions: map[string]*Function{ 865 | "start": { 866 | Definition: &Sentence{Words: []interface{}{"start"}}, 867 | Statements: []Statement{ 868 | &If{ 869 | Question: &Sentence{ 870 | Words: []interface{}{"something"}, 871 | }, 872 | True: &Sentence{ 873 | Words: []interface{}{"next", "line"}, 874 | }, 875 | False: &Sentence{ 876 | Words: []interface{}{"foo"}, 877 | }, 878 | }, 879 | &Sentence{ 880 | Words: []interface{}{"display", "hi"}, 881 | }, 882 | }, 883 | }, 884 | }, 885 | }, 886 | }, 887 | } 888 | 889 | func TestParser_Parse(t *testing.T) { 890 | for testName, test := range parserTests { 891 | t.Run(testName, func(t *testing.T) { 892 | parser := NewParser(strings.NewReader(test.bento)) 893 | actual, err := parser.Parse() 894 | require.NoError(t, err) 895 | 896 | diff := cmp.Diff(test.expected, actual, 897 | cmpopts.AcyclicTransformer("NumberToString", 898 | func(number *Number) string { 899 | return number.String() 900 | })) 901 | 902 | assert.Empty(t, diff) 903 | }) 904 | } 905 | } 906 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strconv" 7 | "syscall" 8 | ) 9 | 10 | // System defines all of the inbuilt functions. 11 | var System = map[string]func(vm *VirtualMachine, args []int){ 12 | // This is a really dodgy hack until we can properly support varargs. Each 13 | // of the arguments will be printed with no space between them and a single 14 | // newline will be written after any (including zero) arguments. 15 | "display": display, 16 | "display ?": display, 17 | "display ? ?": display, 18 | "display ? ? ?": display, 19 | "display ? ? ? ?": display, 20 | "display ? ? ? ? ?": display, 21 | "display ? ? ? ? ? ?": display, 22 | "display ? ? ? ? ? ? ?": display, 23 | "display ? ? ? ? ? ? ? ? ?": display, 24 | "display ? ? ? ? ? ? ? ? ? ?": display, 25 | "display ? ? ? ? ? ? ? ? ? ? ?": display, 26 | 27 | // The other built-in functions. 28 | "set ? to ?": setVariable, 29 | "add ? and ? into ?": add, 30 | "subtract ? from ? into ?": subtract, 31 | "multiply ? and ? into ?": multiply, 32 | "divide ? by ? into ?": divide, 33 | "run system command ?": system, 34 | "run system command ? output into ?": systemOutput, 35 | "run system command ? status code into ?": systemStatus, 36 | "run system command ? output into ? status code into ?": systemOutputStatus, 37 | } 38 | 39 | func display(vm *VirtualMachine, args []int) { 40 | for _, arg := range args { 41 | // TODO: Convert this switch into an interface. 42 | switch value := vm.GetArg(arg).(type) { 43 | case *string: // text 44 | _, _ = fmt.Fprintf(vm.out, "%v", *value) 45 | 46 | case *Number: 47 | _, _ = fmt.Fprintf(vm.out, "%v", value.String()) 48 | 49 | case nil: // blackhole 50 | 51 | case *Backend: 52 | response, err := value.send(&BackendRequest{ 53 | Sentence: "display ?", 54 | Args: []string{fmt.Sprintf("%v", value)}, 55 | }) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | _, _ = fmt.Fprintf(vm.out, "%v", response.Text) 61 | 62 | default: 63 | panic(value) 64 | } 65 | } 66 | 67 | _, _ = fmt.Fprint(vm.out, "\n") 68 | } 69 | 70 | func setVariable(vm *VirtualMachine, args []int) { 71 | switch value := vm.GetArg(args[1]).(type) { 72 | case *string: // text 73 | vm.SetArg(args[0], NewText(*value)) 74 | 75 | case *Number: 76 | vm.GetNumber(args[0]).Set(value) 77 | } 78 | } 79 | 80 | func add(vm *VirtualMachine, args []int) { 81 | a := vm.GetNumber(args[0]) 82 | b := vm.GetNumber(args[1]) 83 | c := vm.GetNumber(args[2]) 84 | c.Add(a, b) 85 | } 86 | 87 | func subtract(vm *VirtualMachine, args []int) { 88 | a := vm.GetNumber(args[0]) 89 | b := vm.GetNumber(args[1]) 90 | c := vm.GetNumber(args[2]) 91 | 92 | // Notice there are in reverse order because the language is 93 | // "subtract a from b". 94 | c.Sub(b, a) 95 | } 96 | 97 | func multiply(vm *VirtualMachine, args []int) { 98 | a := vm.GetNumber(args[0]) 99 | b := vm.GetNumber(args[1]) 100 | c := vm.GetNumber(args[2]) 101 | c.Mul(a, b) 102 | } 103 | 104 | func divide(vm *VirtualMachine, args []int) { 105 | a := vm.GetNumber(args[0]) 106 | b := vm.GetNumber(args[1]) 107 | c := vm.GetNumber(args[2]) 108 | c.Quo(a, b) 109 | } 110 | 111 | func runSystemCommand(rawCommand string) (output []byte, status int) { 112 | cmd := exec.Command("sh", "-c", rawCommand) 113 | var err error 114 | output, err = cmd.CombinedOutput() 115 | 116 | if msg, ok := err.(*exec.ExitError); ok { 117 | status = msg.Sys().(syscall.WaitStatus).ExitStatus() 118 | } 119 | 120 | return 121 | } 122 | 123 | func system(vm *VirtualMachine, args []int) { 124 | rawCommand := vm.GetText(args[0]) 125 | output, _ := runSystemCommand(*rawCommand) 126 | _, _ = vm.out.Write(output) 127 | } 128 | 129 | func systemOutput(vm *VirtualMachine, args []int) { 130 | rawCommand := vm.GetText(args[0]) 131 | output, _ := runSystemCommand(*rawCommand) 132 | vm.SetArg(args[1], NewText(string(output))) 133 | } 134 | 135 | func systemStatus(vm *VirtualMachine, args []int) { 136 | rawCommand := vm.GetText(args[0]) 137 | _, status := runSystemCommand(*rawCommand) 138 | vm.SetArg(args[1], NewNumber(strconv.Itoa(status), 0)) 139 | } 140 | 141 | func systemOutputStatus(vm *VirtualMachine, args []int) { 142 | rawCommand := vm.GetText(args[0]) 143 | output, status := runSystemCommand(*rawCommand) 144 | vm.SetArg(args[1], NewText(string(output))) 145 | vm.SetArg(args[2], NewNumber(strconv.Itoa(status), 0)) 146 | } 147 | -------------------------------------------------------------------------------- /tests/add.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare result is number 3 | 4 | add 1.23 and 7.89 into result 5 | display result 6 | 7 | add 234 and 34 into result 8 | display result 9 | 10 | add result and -34.3424 into result 11 | display result 12 | -------------------------------------------------------------------------------- /tests/add.txt: -------------------------------------------------------------------------------- 1 | 9.12 2 | 268 3 | 233.6576 4 | -------------------------------------------------------------------------------- /tests/blackhole.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare my-name is text 3 | declare result is number 4 | 5 | display _ 6 | 7 | set _ to 123 8 | 9 | set my-name to "Bob" 10 | display my-name 11 | 12 | add _ and 5 into result 13 | display result 14 | 15 | add result and _ into _ 16 | display result 17 | -------------------------------------------------------------------------------- /tests/blackhole.txt: -------------------------------------------------------------------------------- 1 | 2 | Bob 3 | 5 4 | 5 5 | -------------------------------------------------------------------------------- /tests/display.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare my-name is text 3 | 4 | display "Hello, World!" 5 | display "" 6 | display my-name 7 | 8 | set my-name to "Bob" 9 | display my-name 10 | -------------------------------------------------------------------------------- /tests/display.txt: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | 3 | 4 | Bob 5 | -------------------------------------------------------------------------------- /tests/divide.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare result is number 3 | 4 | divide 1.23 by 7.89 into result 5 | display result 6 | 7 | divide 234 by 34 into result 8 | display result 9 | 10 | divide result by -34.3424 into result 11 | display result 12 | -------------------------------------------------------------------------------- /tests/divide.txt: -------------------------------------------------------------------------------- 1 | 0.155894 2 | 6.882353 3 | -0.200404 4 | -------------------------------------------------------------------------------- /tests/functions.bento: -------------------------------------------------------------------------------- 1 | start: 2 | print everything 3 | say "Hi" to "Bob" 4 | 5 | print everything: 6 | display "Hello" 7 | display "World" 8 | 9 | say greeting to persons-name (persons-name is text, greeting is text): 10 | display greeting 11 | display persons-name 12 | -------------------------------------------------------------------------------- /tests/functions.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | World 3 | Hi 4 | Bob 5 | -------------------------------------------------------------------------------- /tests/if.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare my-name is text 3 | 4 | if my-name = "Elliot", display "Hi!", otherwise display "Hello?" 5 | 6 | set my-name to "Bob" 7 | 8 | if my-name = "Bob", display "Hi Bob!" 9 | 10 | if my-name != "John", set my-name to "John" 11 | 12 | if my-name != "John", display "oops!", otherwise display "All good." 13 | -------------------------------------------------------------------------------- /tests/if.txt: -------------------------------------------------------------------------------- 1 | Hello? 2 | Hi Bob! 3 | All good. 4 | -------------------------------------------------------------------------------- /tests/multiline.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare result is number 3 | 4 | display ... 5 | result 6 | -------------------------------------------------------------------------------- /tests/multiline.txt: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /tests/multiply.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare result is number 3 | 4 | multiply 1.23 and 7.89 into result 5 | display result 6 | 7 | multiply 234 and 34 into result 8 | display result 9 | 10 | multiply result and -34.3424 into result 11 | display result 12 | -------------------------------------------------------------------------------- /tests/multiply.txt: -------------------------------------------------------------------------------- 1 | 9.7047 2 | 7956 3 | -273228.1344 4 | -------------------------------------------------------------------------------- /tests/number.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare first-var is a number 3 | declare second-var is a number 4 | 5 | set first-var to 1.23 6 | 7 | display first-var 8 | display second-var 9 | 10 | # Numbers always calculate to the precision of the destination. 11 | declare a is number with 1 decimal place 12 | declare b is number with 1 decimal place 13 | declare c is number with 2 decimal places 14 | 15 | set a to 5.51 # rounded to 5.5 16 | set b to 6.48 # rounded to 6.5 17 | 18 | display a 19 | display b 20 | multiply a and b into c 21 | display c 22 | -------------------------------------------------------------------------------- /tests/number.txt: -------------------------------------------------------------------------------- 1 | 1.23 2 | 0 3 | 5.5 4 | 6.5 5 | 35.75 6 | -------------------------------------------------------------------------------- /tests/question.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare x is number 3 | 4 | if foo bar, display "good 1" 5 | 6 | unless foo bar, display "bad 2" 7 | 8 | set x to 123 9 | if x is over 100, display "good 3", otherwise display "bad 3" 10 | 11 | subtract 50 from x into x 12 | if x is over 100, display "bad 4", otherwise display "good 4" 13 | 14 | if foo bar, display "good 5", otherwise display "bad 5" 15 | 16 | unless foo bar, display "bad 6", otherwise display "good 6" 17 | 18 | foo bar? 19 | yes 20 | 21 | the-number is over threshold (the-number is number, threshold is number)? 22 | if the-number > threshold, yes 23 | -------------------------------------------------------------------------------- /tests/question.txt: -------------------------------------------------------------------------------- 1 | good 1 2 | good 3 3 | good 4 4 | good 5 5 | good 6 6 | -------------------------------------------------------------------------------- /tests/subtract.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare result is number 3 | 4 | subtract 1.23 from 7.89 into result 5 | display result 6 | 7 | subtract 234 from 34 into result 8 | display result 9 | 10 | subtract result from -34.3424 into result 11 | display result 12 | -------------------------------------------------------------------------------- /tests/subtract.txt: -------------------------------------------------------------------------------- 1 | 6.66 2 | -200 3 | 165.6576 4 | -------------------------------------------------------------------------------- /tests/system.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare echo-result is text 3 | declare echo-status is number 4 | 5 | run system command "echo hi" 6 | run system command "nosuchcommand" 7 | 8 | run system command "echo hello" output into echo-result 9 | display "---1" 10 | display echo-result 11 | 12 | run system command "nosuchcommand" status code into echo-status 13 | display "---2" 14 | display echo-status 15 | 16 | run system command "exit 52" output into echo-result status code into echo-status 17 | display "---3" 18 | display echo-result 19 | display echo-status 20 | -------------------------------------------------------------------------------- /tests/system.darwin.txt: -------------------------------------------------------------------------------- 1 | hi 2 | sh: nosuchcommand: command not found 3 | ---1 4 | hello 5 | 6 | ---2 7 | 127 8 | ---3 9 | 10 | 52 11 | -------------------------------------------------------------------------------- /tests/system.linux.txt: -------------------------------------------------------------------------------- 1 | hi 2 | sh: 1: nosuchcommand: not found 3 | ---1 4 | hello 5 | 6 | ---2 7 | 127 8 | ---3 9 | 10 | 52 11 | -------------------------------------------------------------------------------- /tests/text.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare first-var is text 3 | declare second-var is text 4 | 5 | set first-var to "hello" 6 | 7 | display first-var 8 | display second-var 9 | -------------------------------------------------------------------------------- /tests/text.txt: -------------------------------------------------------------------------------- 1 | hello 2 | 3 | -------------------------------------------------------------------------------- /tests/unless.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare my-name is text 3 | 4 | unless my-name = "Elliot", display "Hi!", otherwise display "Hello?" 5 | 6 | set my-name to "Bob" 7 | 8 | unless my-name = "Bob", display "Hi Bob!" 9 | 10 | unless my-name != "John", set my-name to "John" 11 | 12 | unless my-name != "John", display "oops!", otherwise display "All good." 13 | -------------------------------------------------------------------------------- /tests/unless.txt: -------------------------------------------------------------------------------- 1 | Hi! 2 | All good. 3 | -------------------------------------------------------------------------------- /tests/until.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare counter is number 3 | 4 | set counter to 10 5 | 6 | until counter < 3, subtract 2 from counter into counter 7 | display counter 8 | -------------------------------------------------------------------------------- /tests/until.txt: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /tests/while.bento: -------------------------------------------------------------------------------- 1 | start: 2 | declare counter is number 3 | 4 | while counter < 10, add counter and 1 into counter 5 | display counter 6 | -------------------------------------------------------------------------------- /tests/while.txt: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | const ( 11 | TokenKindEndOfFile = "end of file" 12 | TokenKindEndOfLine = "new line" 13 | TokenKindWord = "word" 14 | TokenKindNumber = "number" 15 | TokenKindText = "text" 16 | TokenKindColon = ":" 17 | TokenKindOpenBracket = "(" 18 | TokenKindCloseBracket = ")" 19 | TokenKindComma = "," 20 | TokenKindOperator = "operator" 21 | TokenKindEllipsis = "..." 22 | TokenKindQuestion = "?" 23 | ) 24 | 25 | type Token struct { 26 | Kind string 27 | Value string 28 | } 29 | 30 | func Tokenize(r io.Reader) (tokens []Token, err error) { 31 | data, err := ioutil.ReadAll(r) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | entire := string(data) 37 | 38 | for i := 0; i < len(entire); i++ { 39 | switch entire[i] { 40 | case '.': 41 | // TODO: Check len() allows this. 42 | if entire[i+1] == '.' && entire[i+2] == '.' { 43 | tokens = append(tokens, Token{TokenKindEllipsis, ""}) 44 | i += 2 45 | } 46 | 47 | case ',': 48 | tokens = append(tokens, Token{TokenKindComma, ""}) 49 | 50 | case '(': 51 | tokens = append(tokens, Token{TokenKindOpenBracket, ""}) 52 | 53 | case ')': 54 | tokens = append(tokens, Token{TokenKindCloseBracket, ""}) 55 | 56 | case ':': 57 | tokens = append(tokens, Token{TokenKindColon, ""}) 58 | 59 | case '?': 60 | tokens = append(tokens, Token{TokenKindQuestion, ""}) 61 | 62 | case '=', '!', '>', '<': 63 | var operator string 64 | operator, i = consumeCharacters(isOperatorCharacter, entire, i) 65 | tokens = append(tokens, Token{TokenKindOperator, operator}) 66 | 67 | case '#': 68 | tokens = appendEndOfLine(tokens) 69 | for ; i < len(entire); i++ { 70 | if entire[i] == '\n' { 71 | break 72 | } 73 | } 74 | 75 | case '\n': 76 | tokens = appendEndOfLine(tokens) 77 | 78 | case '"': 79 | i++ 80 | for start := i; i < len(entire); i++ { 81 | if entire[i] == '"' || i == len(entire)-1 { 82 | tokens = append(tokens, 83 | Token{TokenKindText, entire[start:i]}) 84 | break 85 | } 86 | } 87 | 88 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': 89 | var number string 90 | number, i = consumeCharacters(isNumberCharacter, entire, i) 91 | tokens = append(tokens, Token{TokenKindNumber, number}) 92 | 93 | // TODO: Check invalid numbers like 1.2.3 94 | 95 | case ' ', '\t': 96 | // Ignore whitespace. 97 | 98 | default: 99 | // TODO: If nothing is consumed below it will be an infinite loop. 100 | 101 | var word string 102 | word, i = consumeCharacters(isWordCharacter, entire, i) 103 | tokens = append(tokens, Token{TokenKindWord, strings.ToLower(word)}) 104 | } 105 | } 106 | 107 | tokens = appendEndOfLine(tokens) 108 | tokens = append(tokens, Token{TokenKindEndOfFile, ""}) 109 | 110 | return 111 | } 112 | 113 | func consumeCharacters(t func(byte) bool, entire string, i int) (string, int) { 114 | start := i 115 | 116 | for ; i < len(entire); i++ { 117 | if !t(entire[i]) { 118 | break 119 | } 120 | } 121 | 122 | return entire[start:i], i - 1 123 | } 124 | 125 | func isOperatorCharacter(c byte) bool { 126 | return c == '=' || c == '!' || c == '<' || c == '>' 127 | } 128 | 129 | func isNumberCharacter(c byte) bool { 130 | return (c >= '0' && c <= '9') || c == '.' || c == '-' 131 | } 132 | 133 | func isWordCharacter(c byte) bool { 134 | // TODO: This will not work with unicode characters. 135 | return unicode.IsLetter(rune(c)) || 136 | unicode.IsDigit(rune(c)) || 137 | c == '-' || 138 | c == '_' 139 | } 140 | 141 | func appendEndOfLine(tokens []Token) []Token { 142 | if len(tokens) > 0 && tokens[len(tokens)-1].Kind != TokenKindEndOfLine { 143 | return append(tokens, Token{TokenKindEndOfLine, ""}) 144 | } 145 | 146 | return tokens 147 | } 148 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/require" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestTokenize(t *testing.T) { 11 | for testName, test := range map[string]struct { 12 | bento string 13 | expected []Token 14 | }{ 15 | "Empty": { 16 | bento: "", 17 | expected: []Token{ 18 | {TokenKindEndOfFile, ""}, 19 | }, 20 | }, 21 | "Word": { 22 | bento: "hello", 23 | expected: []Token{ 24 | {TokenKindWord, "hello"}, 25 | {TokenKindEndOfLine, ""}, 26 | {TokenKindEndOfFile, ""}, 27 | }, 28 | }, 29 | "TwoWords": { 30 | bento: "hello world", 31 | expected: []Token{ 32 | {TokenKindWord, "hello"}, 33 | {TokenKindWord, "world"}, 34 | {TokenKindEndOfLine, ""}, 35 | {TokenKindEndOfFile, ""}, 36 | }, 37 | }, 38 | "Mix1": { 39 | bento: `display "hello"`, 40 | expected: []Token{ 41 | {TokenKindWord, "display"}, 42 | {TokenKindText, "hello"}, 43 | {TokenKindEndOfLine, ""}, 44 | {TokenKindEndOfFile, ""}, 45 | }, 46 | }, 47 | "Mix2": { 48 | bento: `display "hello" ok`, 49 | expected: []Token{ 50 | {TokenKindWord, "display"}, 51 | {TokenKindText, "hello"}, 52 | {TokenKindWord, "ok"}, 53 | {TokenKindEndOfLine, ""}, 54 | {TokenKindEndOfFile, ""}, 55 | }, 56 | }, 57 | "AlwaysLowerCase": { 58 | bento: `Words in MIXED "Case"`, 59 | expected: []Token{ 60 | {TokenKindWord, "words"}, 61 | {TokenKindWord, "in"}, 62 | {TokenKindWord, "mixed"}, 63 | {TokenKindText, "Case"}, 64 | {TokenKindEndOfLine, ""}, 65 | {TokenKindEndOfFile, ""}, 66 | }, 67 | }, 68 | "MultipleSpaces": { 69 | bento: ` foo bar " baz qux" quux `, 70 | expected: []Token{ 71 | {TokenKindWord, "foo"}, 72 | {TokenKindWord, "bar"}, 73 | {TokenKindText, " baz qux"}, 74 | {TokenKindWord, "quux"}, 75 | {TokenKindEndOfLine, ""}, 76 | {TokenKindEndOfFile, ""}, 77 | }, 78 | }, 79 | "Newlines": { 80 | bento: "foo\nbar\n\nbaz\n", 81 | expected: []Token{ 82 | {TokenKindWord, "foo"}, 83 | {TokenKindEndOfLine, ""}, 84 | {TokenKindWord, "bar"}, 85 | {TokenKindEndOfLine, ""}, 86 | {TokenKindWord, "baz"}, 87 | {TokenKindEndOfLine, ""}, 88 | {TokenKindEndOfFile, ""}, 89 | }, 90 | }, 91 | "BeginNewline": { 92 | bento: "\n\nfoo\nbar", 93 | expected: []Token{ 94 | {TokenKindWord, "foo"}, 95 | {TokenKindEndOfLine, ""}, 96 | {TokenKindWord, "bar"}, 97 | {TokenKindEndOfLine, ""}, 98 | {TokenKindEndOfFile, ""}, 99 | }, 100 | }, 101 | "DisplayTwice": { 102 | bento: "Display \"hello\"\ndisplay \"twice!\"", 103 | expected: []Token{ 104 | {TokenKindWord, "display"}, 105 | {TokenKindText, "hello"}, 106 | {TokenKindEndOfLine, ""}, 107 | {TokenKindWord, "display"}, 108 | {TokenKindText, "twice!"}, 109 | {TokenKindEndOfLine, ""}, 110 | {TokenKindEndOfFile, ""}, 111 | }, 112 | }, 113 | "Comment1": { 114 | bento: "# comment", 115 | expected: []Token{ 116 | {TokenKindEndOfFile, ""}, 117 | }, 118 | }, 119 | "Comment2": { 120 | bento: "# comment\ndisplay", 121 | expected: []Token{ 122 | {TokenKindWord, "display"}, 123 | {TokenKindEndOfLine, ""}, 124 | {TokenKindEndOfFile, ""}, 125 | }, 126 | }, 127 | "Comment3": { 128 | bento: "display #comment\ndisplay", 129 | expected: []Token{ 130 | {TokenKindWord, "display"}, 131 | {TokenKindEndOfLine, ""}, 132 | {TokenKindWord, "display"}, 133 | {TokenKindEndOfLine, ""}, 134 | {TokenKindEndOfFile, ""}, 135 | }, 136 | }, 137 | "Function1": { 138 | bento: "do something:\nsomething else", 139 | expected: []Token{ 140 | {TokenKindWord, "do"}, 141 | {TokenKindWord, "something"}, 142 | {TokenKindColon, ""}, 143 | {TokenKindEndOfLine, ""}, 144 | {TokenKindWord, "something"}, 145 | {TokenKindWord, "else"}, 146 | {TokenKindEndOfLine, ""}, 147 | {TokenKindEndOfFile, ""}, 148 | }, 149 | }, 150 | "Function2": { 151 | bento: "do something: foo else", 152 | expected: []Token{ 153 | {TokenKindWord, "do"}, 154 | {TokenKindWord, "something"}, 155 | {TokenKindColon, ""}, 156 | {TokenKindWord, "foo"}, 157 | {TokenKindWord, "else"}, 158 | {TokenKindEndOfLine, ""}, 159 | {TokenKindEndOfFile, ""}, 160 | }, 161 | }, 162 | "Tabs": { 163 | bento: ` foo bar "baz " `, 164 | expected: []Token{ 165 | {TokenKindWord, "foo"}, 166 | {TokenKindWord, "bar"}, 167 | {TokenKindText, "baz "}, 168 | {TokenKindEndOfLine, ""}, 169 | {TokenKindEndOfFile, ""}, 170 | }, 171 | }, 172 | "FunctionWithArgument": { 173 | bento: `greet persons-name now (persons-name is text):`, 174 | expected: []Token{ 175 | {TokenKindWord, "greet"}, 176 | {TokenKindWord, "persons-name"}, 177 | {TokenKindWord, "now"}, 178 | {TokenKindOpenBracket, ""}, 179 | {TokenKindWord, "persons-name"}, 180 | {TokenKindWord, "is"}, 181 | {TokenKindWord, "text"}, 182 | {TokenKindCloseBracket, ""}, 183 | {TokenKindColon, ""}, 184 | {TokenKindEndOfLine, ""}, 185 | {TokenKindEndOfFile, ""}, 186 | }, 187 | }, 188 | "FunctionWithArguments": { 189 | bento: `say greeting to persons-name (persons-name is text, greeting is text):`, 190 | expected: []Token{ 191 | {TokenKindWord, "say"}, 192 | {TokenKindWord, "greeting"}, 193 | {TokenKindWord, "to"}, 194 | {TokenKindWord, "persons-name"}, 195 | {TokenKindOpenBracket, ""}, 196 | {TokenKindWord, "persons-name"}, 197 | {TokenKindWord, "is"}, 198 | {TokenKindWord, "text"}, 199 | {TokenKindComma, ""}, 200 | {TokenKindWord, "greeting"}, 201 | {TokenKindWord, "is"}, 202 | {TokenKindWord, "text"}, 203 | {TokenKindCloseBracket, ""}, 204 | {TokenKindColon, ""}, 205 | {TokenKindEndOfLine, ""}, 206 | {TokenKindEndOfFile, ""}, 207 | }, 208 | }, 209 | "ColonNewline": { 210 | bento: "start:\nDisplay \"Hello, World!\"", 211 | expected: []Token{ 212 | {TokenKindWord, "start"}, 213 | {TokenKindColon, ""}, 214 | {TokenKindEndOfLine, ""}, 215 | {TokenKindWord, "display"}, 216 | {TokenKindText, "Hello, World!"}, 217 | {TokenKindEndOfLine, ""}, 218 | {TokenKindEndOfFile, ""}, 219 | }, 220 | }, 221 | "DeclareNumber": { 222 | bento: "declare foo is number", 223 | expected: []Token{ 224 | {TokenKindWord, "declare"}, 225 | {TokenKindWord, "foo"}, 226 | {TokenKindWord, "is"}, 227 | {TokenKindWord, "number"}, 228 | {TokenKindEndOfLine, ""}, 229 | {TokenKindEndOfFile, ""}, 230 | }, 231 | }, 232 | "Zero": { 233 | bento: "set foo to 0", 234 | expected: []Token{ 235 | {TokenKindWord, "set"}, 236 | {TokenKindWord, "foo"}, 237 | {TokenKindWord, "to"}, 238 | {TokenKindNumber, "0"}, 239 | {TokenKindEndOfLine, ""}, 240 | {TokenKindEndOfFile, ""}, 241 | }, 242 | }, 243 | "Integer": { 244 | bento: "set foo to 123", 245 | expected: []Token{ 246 | {TokenKindWord, "set"}, 247 | {TokenKindWord, "foo"}, 248 | {TokenKindWord, "to"}, 249 | {TokenKindNumber, "123"}, 250 | {TokenKindEndOfLine, ""}, 251 | {TokenKindEndOfFile, ""}, 252 | }, 253 | }, 254 | "FloatNoNewLine": { 255 | bento: "set foo to 1.23", 256 | expected: []Token{ 257 | {TokenKindWord, "set"}, 258 | {TokenKindWord, "foo"}, 259 | {TokenKindWord, "to"}, 260 | {TokenKindNumber, "1.23"}, 261 | {TokenKindEndOfLine, ""}, 262 | {TokenKindEndOfFile, ""}, 263 | }, 264 | }, 265 | "Float": { 266 | bento: "set foo to 1.23\n", 267 | expected: []Token{ 268 | {TokenKindWord, "set"}, 269 | {TokenKindWord, "foo"}, 270 | {TokenKindWord, "to"}, 271 | {TokenKindNumber, "1.23"}, 272 | {TokenKindEndOfLine, ""}, 273 | {TokenKindEndOfFile, ""}, 274 | }, 275 | }, 276 | "TextNoNewLine": { 277 | bento: `"hello"`, 278 | expected: []Token{ 279 | {TokenKindText, "hello"}, 280 | {TokenKindEndOfLine, ""}, 281 | {TokenKindEndOfFile, ""}, 282 | }, 283 | }, 284 | "NegativeFloat": { 285 | bento: "set foo to -1.23", 286 | expected: []Token{ 287 | {TokenKindWord, "set"}, 288 | {TokenKindWord, "foo"}, 289 | {TokenKindWord, "to"}, 290 | {TokenKindNumber, "-1.23"}, 291 | {TokenKindEndOfLine, ""}, 292 | {TokenKindEndOfFile, ""}, 293 | }, 294 | }, 295 | "Equals": { 296 | bento: `foo = "qux"`, 297 | expected: []Token{ 298 | {TokenKindWord, "foo"}, 299 | {TokenKindOperator, "="}, 300 | {TokenKindText, "qux"}, 301 | {TokenKindEndOfLine, ""}, 302 | {TokenKindEndOfFile, ""}, 303 | }, 304 | }, 305 | "NotEquals": { 306 | bento: `foo != "qux"`, 307 | expected: []Token{ 308 | {TokenKindWord, "foo"}, 309 | {TokenKindOperator, "!="}, 310 | {TokenKindText, "qux"}, 311 | {TokenKindEndOfLine, ""}, 312 | {TokenKindEndOfFile, ""}, 313 | }, 314 | }, 315 | "GreaterThan": { 316 | bento: `23 > 1.23`, 317 | expected: []Token{ 318 | {TokenKindNumber, "23"}, 319 | {TokenKindOperator, ">"}, 320 | {TokenKindNumber, "1.23"}, 321 | {TokenKindEndOfLine, ""}, 322 | {TokenKindEndOfFile, ""}, 323 | }, 324 | }, 325 | "GreaterThanEqual": { 326 | bento: `23 >= 1.23`, 327 | expected: []Token{ 328 | {TokenKindNumber, "23"}, 329 | {TokenKindOperator, ">="}, 330 | {TokenKindNumber, "1.23"}, 331 | {TokenKindEndOfLine, ""}, 332 | {TokenKindEndOfFile, ""}, 333 | }, 334 | }, 335 | "LessThan": { 336 | bento: `23 < 1.23`, 337 | expected: []Token{ 338 | {TokenKindNumber, "23"}, 339 | {TokenKindOperator, "<"}, 340 | {TokenKindNumber, "1.23"}, 341 | {TokenKindEndOfLine, ""}, 342 | {TokenKindEndOfFile, ""}, 343 | }, 344 | }, 345 | "LessThanEqual": { 346 | bento: `23 <= 1.23`, 347 | expected: []Token{ 348 | {TokenKindNumber, "23"}, 349 | {TokenKindOperator, "<="}, 350 | {TokenKindNumber, "1.23"}, 351 | {TokenKindEndOfLine, ""}, 352 | {TokenKindEndOfFile, ""}, 353 | }, 354 | }, 355 | "InlineIf": { 356 | bento: "start: if foo = \"qux\", quux 1.234\ncorge", 357 | expected: []Token{ 358 | {TokenKindWord, "start"}, 359 | {TokenKindColon, ""}, 360 | {TokenKindWord, "if"}, 361 | {TokenKindWord, "foo"}, 362 | {TokenKindOperator, "="}, 363 | {TokenKindText, "qux"}, 364 | {TokenKindComma, ""}, 365 | {TokenKindWord, "quux"}, 366 | {TokenKindNumber, "1.234"}, 367 | {TokenKindEndOfLine, ""}, 368 | {TokenKindWord, "corge"}, 369 | {TokenKindEndOfLine, ""}, 370 | {TokenKindEndOfFile, ""}, 371 | }, 372 | }, 373 | "InlineIfElse": { 374 | bento: "start: if foo = \"qux\", quux 1.234, otherwise corge\ndisplay", 375 | expected: []Token{ 376 | {TokenKindWord, "start"}, 377 | {TokenKindColon, ""}, 378 | {TokenKindWord, "if"}, 379 | {TokenKindWord, "foo"}, 380 | {TokenKindOperator, "="}, 381 | {TokenKindText, "qux"}, 382 | {TokenKindComma, ""}, 383 | {TokenKindWord, "quux"}, 384 | {TokenKindNumber, "1.234"}, 385 | {TokenKindComma, ""}, 386 | {TokenKindWord, "otherwise"}, 387 | {TokenKindWord, "corge"}, 388 | {TokenKindEndOfLine, ""}, 389 | {TokenKindWord, "display"}, 390 | {TokenKindEndOfLine, ""}, 391 | {TokenKindEndOfFile, ""}, 392 | }, 393 | }, 394 | "InlineUnless": { 395 | bento: "start: unless foo = \"qux\", quux 1.234\ncorge", 396 | expected: []Token{ 397 | {TokenKindWord, "start"}, 398 | {TokenKindColon, ""}, 399 | {TokenKindWord, "unless"}, 400 | {TokenKindWord, "foo"}, 401 | {TokenKindOperator, "="}, 402 | {TokenKindText, "qux"}, 403 | {TokenKindComma, ""}, 404 | {TokenKindWord, "quux"}, 405 | {TokenKindNumber, "1.234"}, 406 | {TokenKindEndOfLine, ""}, 407 | {TokenKindWord, "corge"}, 408 | {TokenKindEndOfLine, ""}, 409 | {TokenKindEndOfFile, ""}, 410 | }, 411 | }, 412 | "InlineUnlessElse": { 413 | bento: "start: unless foo = \"qux\", quux 1.234, otherwise corge\ndisplay", 414 | expected: []Token{ 415 | {TokenKindWord, "start"}, 416 | {TokenKindColon, ""}, 417 | {TokenKindWord, "unless"}, 418 | {TokenKindWord, "foo"}, 419 | {TokenKindOperator, "="}, 420 | {TokenKindText, "qux"}, 421 | {TokenKindComma, ""}, 422 | {TokenKindWord, "quux"}, 423 | {TokenKindNumber, "1.234"}, 424 | {TokenKindComma, ""}, 425 | {TokenKindWord, "otherwise"}, 426 | {TokenKindWord, "corge"}, 427 | {TokenKindEndOfLine, ""}, 428 | {TokenKindWord, "display"}, 429 | {TokenKindEndOfLine, ""}, 430 | {TokenKindEndOfFile, ""}, 431 | }, 432 | }, 433 | "InlineWhile": { 434 | bento: "start: while i < 10, quux 1.234\ncorge", 435 | expected: []Token{ 436 | {TokenKindWord, "start"}, 437 | {TokenKindColon, ""}, 438 | {TokenKindWord, "while"}, 439 | {TokenKindWord, "i"}, 440 | {TokenKindOperator, "<"}, 441 | {TokenKindNumber, "10"}, 442 | {TokenKindComma, ""}, 443 | {TokenKindWord, "quux"}, 444 | {TokenKindNumber, "1.234"}, 445 | {TokenKindEndOfLine, ""}, 446 | {TokenKindWord, "corge"}, 447 | {TokenKindEndOfLine, ""}, 448 | {TokenKindEndOfFile, ""}, 449 | }, 450 | }, 451 | "InlineUntil": { 452 | bento: "start: until i < 10, quux 1.234\ncorge", 453 | expected: []Token{ 454 | {TokenKindWord, "start"}, 455 | {TokenKindColon, ""}, 456 | {TokenKindWord, "until"}, 457 | {TokenKindWord, "i"}, 458 | {TokenKindOperator, "<"}, 459 | {TokenKindNumber, "10"}, 460 | {TokenKindComma, ""}, 461 | {TokenKindWord, "quux"}, 462 | {TokenKindNumber, "1.234"}, 463 | {TokenKindEndOfLine, ""}, 464 | {TokenKindWord, "corge"}, 465 | {TokenKindEndOfLine, ""}, 466 | {TokenKindEndOfFile, ""}, 467 | }, 468 | }, 469 | "MultilineSentence1": { 470 | bento: "start: foo bar...\n baz", 471 | expected: []Token{ 472 | {TokenKindWord, "start"}, 473 | {TokenKindColon, ""}, 474 | {TokenKindWord, "foo"}, 475 | {TokenKindWord, "bar"}, 476 | {TokenKindEllipsis, ""}, 477 | {TokenKindEndOfLine, ""}, 478 | {TokenKindWord, "baz"}, 479 | {TokenKindEndOfLine, ""}, 480 | {TokenKindEndOfFile, ""}, 481 | }, 482 | }, 483 | "MultilineSentence2": { 484 | bento: "start: foo bar\t ... \n baz", 485 | expected: []Token{ 486 | {TokenKindWord, "start"}, 487 | {TokenKindColon, ""}, 488 | {TokenKindWord, "foo"}, 489 | {TokenKindWord, "bar"}, 490 | {TokenKindEllipsis, ""}, 491 | {TokenKindEndOfLine, ""}, 492 | {TokenKindWord, "baz"}, 493 | {TokenKindEndOfLine, ""}, 494 | {TokenKindEndOfFile, ""}, 495 | }, 496 | }, 497 | "BlackholeVariable": { 498 | bento: "start: display _", 499 | expected: []Token{ 500 | {TokenKindWord, "start"}, 501 | {TokenKindColon, ""}, 502 | {TokenKindWord, "display"}, 503 | {TokenKindWord, "_"}, 504 | {TokenKindEndOfLine, ""}, 505 | {TokenKindEndOfFile, ""}, 506 | }, 507 | }, 508 | "VariableStartingWithUnderscore": { 509 | bento: "start: display _foo bar", 510 | expected: []Token{ 511 | {TokenKindWord, "start"}, 512 | {TokenKindColon, ""}, 513 | {TokenKindWord, "display"}, 514 | {TokenKindWord, "_foo"}, 515 | {TokenKindWord, "bar"}, 516 | {TokenKindEndOfLine, ""}, 517 | {TokenKindEndOfFile, ""}, 518 | }, 519 | }, 520 | "QuestionDefinition": { 521 | bento: "is good?\nyes", 522 | expected: []Token{ 523 | {TokenKindWord, "is"}, 524 | {TokenKindWord, "good"}, 525 | {TokenKindQuestion, ""}, 526 | {TokenKindEndOfLine, ""}, 527 | {TokenKindWord, "yes"}, 528 | {TokenKindEndOfLine, ""}, 529 | {TokenKindEndOfFile, ""}, 530 | }, 531 | }, 532 | } { 533 | t.Run(testName, func(t *testing.T) { 534 | actual, err := Tokenize(strings.NewReader(test.bento)) 535 | require.NoError(t, err) 536 | 537 | assert.Equal(t, test.expected, actual) 538 | }) 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /variables.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | VariableTypeBlackhole = "blackhole" 5 | VariableTypeText = "text" 6 | VariableTypeNumber = "number" 7 | ) 8 | 9 | type VariableDefinition struct { 10 | Name string 11 | Type string 12 | 13 | // LocalScope is true if the variable was declared within the function. 14 | LocalScope bool 15 | 16 | // Precision is the decimal places for "number" type. 17 | Precision int 18 | } 19 | 20 | type VariableReference string 21 | 22 | var BlackholeVariable = VariableReference("_") 23 | 24 | var blackholeVariableIndex = -1 25 | 26 | func NewText(s string) *string { 27 | return &s 28 | } 29 | -------------------------------------------------------------------------------- /vm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type Instruction interface{} 13 | 14 | type ConditionJumpInstruction struct { 15 | Left, Right int 16 | Operator string 17 | True, False int 18 | } 19 | 20 | type QuestionJumpInstruction struct { 21 | True, False int 22 | } 23 | 24 | type JumpInstruction struct { 25 | Forward int 26 | } 27 | 28 | type CallInstruction struct { 29 | Call string 30 | Args []int 31 | } 32 | 33 | type QuestionAnswerInstruction struct { 34 | Yes bool 35 | } 36 | 37 | type VirtualMachine struct { 38 | program *CompiledProgram 39 | memory []interface{} 40 | stackOffset []int 41 | out io.Writer 42 | answer bool 43 | backends []*Backend 44 | } 45 | 46 | func NewVirtualMachine(program *CompiledProgram) *VirtualMachine { 47 | return &VirtualMachine{ 48 | program: program, 49 | out: os.Stdout, 50 | } 51 | } 52 | 53 | func (vm *VirtualMachine) Run() error { 54 | vm.stackOffset = []int{0} 55 | 56 | // TODO: Check start exists. 57 | return vm.call("start", nil) 58 | } 59 | 60 | func (vm *VirtualMachine) call(syntax string, args []int) error { 61 | fn := vm.program.Functions[syntax] 62 | 63 | if fn == nil { 64 | // Maybe it belongs to a backend? 65 | for _, arg := range args { 66 | // TODO: It is ambiguous if a sentence contains more than one 67 | // backend. 68 | if backend, ok := vm.GetArg(arg).(*Backend); ok { 69 | // TODO: Make sure syntax exists 70 | var realArgs []string 71 | for _, realArg := range args { 72 | realArgs = append(realArgs, fmt.Sprintf("%v", vm.GetArg(realArg))) 73 | } 74 | result, err := backend.send(&BackendRequest{ 75 | Sentence: syntax, 76 | Args: realArgs, 77 | }) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | for key, value := range result.Set { 83 | index, err := strconv.Atoi(key[1:]) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | vm.SetArg(index, NewText(value)) 89 | } 90 | 91 | return nil 92 | } 93 | } 94 | 95 | return fmt.Errorf("no such function: %s", syntax) 96 | } 97 | 98 | // Start backends. 99 | // TODO: Backends are not closed. 100 | for _, variable := range fn.Variables { 101 | if backend, ok := variable.(*Backend); ok { 102 | err := backend.Start() 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | } 108 | 109 | fn.InstructionOffset = 0 110 | 111 | // Expand the memory to accommodate the variables (arguments and constants 112 | // used in the function). 113 | // TODO: Refactor this in a much more efficient way. 114 | offset := vm.stackOffset[len(vm.stackOffset)-1] 115 | for i, v := range fn.Variables { 116 | for offset+i >= len(vm.memory) { 117 | vm.memory = append(vm.memory, nil) 118 | } 119 | vm.memory[offset+i] = v 120 | } 121 | 122 | // Load in the arguments. 123 | for i, arg := range args { 124 | to := vm.stackOffset[len(vm.stackOffset)-1] + i 125 | from := vm.stackOffset[len(vm.stackOffset)-2] + arg 126 | vm.memory[to] = vm.memory[from] 127 | } 128 | 129 | vm.stackOffset = append(vm.stackOffset, 130 | vm.stackOffset[len(vm.stackOffset)-1]+len(fn.Variables)) 131 | 132 | for fn.InstructionOffset < len(fn.Instructions) { 133 | instruction := fn.Instructions[fn.InstructionOffset] 134 | 135 | var move int 136 | var err error 137 | 138 | // TODO: This switch needs to be refactored into an interface. 139 | switch ins := instruction.(type) { 140 | case *CallInstruction: 141 | move, err = vm.callInstruction(ins) 142 | 143 | case *ConditionJumpInstruction: 144 | move, err = vm.conditionJumpInstruction(ins) 145 | 146 | case *JumpInstruction: 147 | move, err = vm.jumpInstruction(ins) 148 | 149 | case *QuestionJumpInstruction: 150 | move, err = vm.questionJumpInstruction(ins) 151 | 152 | case *QuestionAnswerInstruction: 153 | move, err = vm.questionAnswerInstruction(ins) 154 | 155 | default: 156 | panic(ins) 157 | } 158 | 159 | if err != nil { 160 | return err 161 | } 162 | 163 | fn.InstructionOffset += move 164 | } 165 | 166 | vm.stackOffset = vm.stackOffset[:len(vm.stackOffset)-1] 167 | 168 | return nil 169 | } 170 | 171 | func (vm *VirtualMachine) questionAnswerInstruction(instruction *QuestionAnswerInstruction) (int, error) { 172 | vm.answer = instruction.Yes 173 | 174 | return 1, nil 175 | } 176 | 177 | func (vm *VirtualMachine) conditionJumpInstruction(instruction *ConditionJumpInstruction) (int, error) { 178 | cmp := 0 179 | left := vm.GetArg(instruction.Left) 180 | right := vm.GetArg(instruction.Right) 181 | 182 | leftText, leftIsText := left.(*string) 183 | rightText, rightIsText := right.(*string) 184 | 185 | if leftIsText && rightIsText { 186 | cmp = strings.Compare(*leftText, *rightText) 187 | goto done 188 | } else { 189 | leftNumber, leftIsNumber := left.(*Number) 190 | rightNumber, rightIsNumber := right.(*Number) 191 | 192 | if leftIsNumber && rightIsNumber { 193 | cmp = leftNumber.Cmp(rightNumber) 194 | goto done 195 | } 196 | } 197 | 198 | return 0, fmt.Errorf("cannot compare: %s %s %s", 199 | vm.GetArgType(instruction.Left), 200 | instruction.Operator, 201 | vm.GetArgType(instruction.Right)) 202 | 203 | done: 204 | var result bool 205 | switch instruction.Operator { 206 | case OperatorEqual: 207 | result = cmp == 0 208 | 209 | case OperatorNotEqual: 210 | result = cmp != 0 211 | 212 | case OperatorGreaterThan: 213 | result = cmp > 0 214 | 215 | case OperatorGreaterThanEqual: 216 | result = cmp >= 0 217 | 218 | case OperatorLessThan: 219 | result = cmp < 0 220 | 221 | case OperatorLessThanEqual: 222 | result = cmp <= 0 223 | } 224 | 225 | if result { 226 | return instruction.True, nil 227 | } 228 | 229 | return instruction.False, nil 230 | } 231 | 232 | func (vm *VirtualMachine) questionJumpInstruction(instruction *QuestionJumpInstruction) (int, error) { 233 | if vm.answer { 234 | return instruction.True, nil 235 | } 236 | 237 | return instruction.False, nil 238 | } 239 | 240 | func (vm *VirtualMachine) jumpInstruction(instruction *JumpInstruction) (int, error) { 241 | return instruction.Forward, nil 242 | } 243 | 244 | func (vm *VirtualMachine) callInstruction(instruction *CallInstruction) (int, error) { 245 | // We technically only need to do this when calling a question. 246 | vm.answer = false 247 | 248 | // Check if it is a system call? 249 | if handler, ok := System[instruction.Call]; ok { 250 | handler(vm, instruction.Args) 251 | 252 | return 1, nil 253 | } 254 | 255 | // Otherwise we have to increase the stack. 256 | return 1, vm.call(instruction.Call, instruction.Args) 257 | } 258 | 259 | func (vm *VirtualMachine) GetArg(index int) interface{} { 260 | if index == blackholeVariableIndex { 261 | return nil 262 | } 263 | 264 | return vm.memory[vm.previousOffset()+index] 265 | } 266 | 267 | func (vm *VirtualMachine) previousOffset() int { 268 | return vm.stackOffset[len(vm.stackOffset)-2] 269 | } 270 | 271 | func (vm *VirtualMachine) SetArg(index int, value interface{}) { 272 | if index == blackholeVariableIndex { 273 | return 274 | } 275 | 276 | vm.memory[vm.previousOffset()+index] = value 277 | } 278 | 279 | func (vm *VirtualMachine) GetNumber(index int) *Number { 280 | if index == blackholeVariableIndex { 281 | return NewNumber("0", DefaultNumericPrecision) 282 | } 283 | 284 | return vm.memory[vm.previousOffset()+index].(*Number) 285 | } 286 | 287 | func (vm *VirtualMachine) GetText(index int) *string { 288 | if index == blackholeVariableIndex { 289 | return NewText("") 290 | } 291 | 292 | return vm.memory[vm.previousOffset()+index].(*string) 293 | } 294 | 295 | func (vm *VirtualMachine) GetArgType(index int) string { 296 | switch vm.GetArg(index).(type) { 297 | case nil: 298 | return VariableTypeBlackhole 299 | 300 | case *string: 301 | return VariableTypeText 302 | 303 | case *Number: 304 | return VariableTypeNumber 305 | } 306 | 307 | return reflect.TypeOf(vm.GetArg(index)).String() 308 | } 309 | -------------------------------------------------------------------------------- /vm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var vmTests = map[string]struct { 12 | program *CompiledProgram 13 | expectedMemory []interface{} 14 | expectedOutput string 15 | }{ 16 | "Simple": { 17 | program: &CompiledProgram{ 18 | Functions: map[string]*CompiledFunction{ 19 | "start": { 20 | Variables: []interface{}{ 21 | NewText("hello"), 22 | }, 23 | Instructions: []Instruction{ 24 | &CallInstruction{ 25 | Call: "display ?", 26 | Args: []int{0}, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | expectedMemory: []interface{}{ 33 | NewText("hello"), 34 | }, 35 | expectedOutput: "hello\n", 36 | }, 37 | "Call1": { 38 | program: &CompiledProgram{ 39 | Functions: map[string]*CompiledFunction{ 40 | "start": { 41 | Variables: []interface{}{ 42 | NewText("Bob"), 43 | }, 44 | Instructions: []Instruction{ 45 | &CallInstruction{ 46 | Call: "print ?", 47 | Args: []int{0}, 48 | }, 49 | }, 50 | }, 51 | "print ?": { 52 | Variables: []interface{}{ 53 | nil, NewText("hi"), 54 | }, 55 | Instructions: []Instruction{ 56 | &CallInstruction{ 57 | Call: "display ?", 58 | Args: []int{1}, 59 | }, 60 | &CallInstruction{ 61 | Call: "display ?", 62 | Args: []int{0}, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | expectedMemory: []interface{}{ 69 | NewText("Bob"), // start 70 | NewText("Bob"), NewText("hi"), // print ? 71 | }, 72 | expectedOutput: "hi\nBob\n", 73 | }, 74 | "SetText": { 75 | program: &CompiledProgram{ 76 | Functions: map[string]*CompiledFunction{ 77 | "start": { 78 | Variables: []interface{}{ 79 | NewText(""), NewText("foo"), 80 | }, 81 | Instructions: []Instruction{ 82 | &CallInstruction{ 83 | Call: "set ? to ?", 84 | Args: []int{0, 1}, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | expectedMemory: []interface{}{ 91 | NewText("foo"), NewText("foo"), // start 92 | }, 93 | }, 94 | "SetNumber": { 95 | program: &CompiledProgram{ 96 | Functions: map[string]*CompiledFunction{ 97 | "start": { 98 | Variables: []interface{}{ 99 | NewNumber("0", 6), NewNumber("1.23", 6), 100 | }, 101 | Instructions: []Instruction{ 102 | &CallInstruction{ 103 | Call: "set ? to ?", 104 | Args: []int{0, 1}, 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | expectedMemory: []interface{}{ 111 | NewNumber("1.23", 6), NewNumber("1.23", 6), // start 112 | }, 113 | }, 114 | "InlineIfTrue": { 115 | program: &CompiledProgram{ 116 | Functions: map[string]*CompiledFunction{ 117 | "start": { 118 | Variables: []interface{}{ 119 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("done"), 120 | }, 121 | Instructions: []Instruction{ 122 | &ConditionJumpInstruction{ 123 | Left: 0, 124 | Right: 1, 125 | Operator: OperatorEqual, 126 | True: 1, 127 | False: 2, 128 | }, 129 | &CallInstruction{ 130 | Call: "display ?", 131 | Args: []int{2}, 132 | }, 133 | &CallInstruction{ 134 | Call: "display ?", 135 | Args: []int{3}, 136 | }, 137 | }, 138 | }, 139 | }, 140 | }, 141 | expectedMemory: []interface{}{ 142 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("done"), // start 143 | }, 144 | expectedOutput: "match!\ndone\n", 145 | }, 146 | "InlineIfFalse": { 147 | program: &CompiledProgram{ 148 | Functions: map[string]*CompiledFunction{ 149 | "start": { 150 | Variables: []interface{}{ 151 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 152 | }, 153 | Instructions: []Instruction{ 154 | &ConditionJumpInstruction{ 155 | Left: 0, 156 | Right: 1, 157 | Operator: OperatorEqual, 158 | True: 1, 159 | False: 2, 160 | }, 161 | &CallInstruction{ 162 | Call: "display ?", 163 | Args: []int{2}, 164 | }, 165 | &CallInstruction{ 166 | Call: "display ?", 167 | Args: []int{3}, 168 | }, 169 | }, 170 | }, 171 | }, 172 | }, 173 | expectedMemory: []interface{}{ 174 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), // start 175 | }, 176 | expectedOutput: "done\n", 177 | }, 178 | "InlineIfElseTrue": { 179 | program: &CompiledProgram{ 180 | Functions: map[string]*CompiledFunction{ 181 | "start": { 182 | Variables: []interface{}{ 183 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("no match!"), NewText("done"), 184 | }, 185 | Instructions: []Instruction{ 186 | &ConditionJumpInstruction{ 187 | Left: 0, 188 | Right: 1, 189 | Operator: OperatorEqual, 190 | True: 1, 191 | False: 2, 192 | }, 193 | &CallInstruction{ 194 | Call: "display ?", 195 | Args: []int{2}, 196 | }, 197 | &JumpInstruction{ 198 | Forward: 2, 199 | }, 200 | &CallInstruction{ 201 | Call: "display ?", 202 | Args: []int{3}, 203 | }, 204 | &CallInstruction{ 205 | Call: "display ?", 206 | Args: []int{4}, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | expectedMemory: []interface{}{ 213 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("no match!"), NewText("done"), // start 214 | }, 215 | expectedOutput: "match!\ndone\n", 216 | }, 217 | "InlineUnlessTrue": { 218 | program: &CompiledProgram{ 219 | Functions: map[string]*CompiledFunction{ 220 | "start": { 221 | Variables: []interface{}{ 222 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("done"), 223 | }, 224 | Instructions: []Instruction{ 225 | &ConditionJumpInstruction{ 226 | Left: 0, 227 | Right: 1, 228 | Operator: OperatorEqual, 229 | True: 2, 230 | False: 1, 231 | }, 232 | &CallInstruction{ 233 | Call: "display ?", 234 | Args: []int{2}, 235 | }, 236 | &CallInstruction{ 237 | Call: "display ?", 238 | Args: []int{3}, 239 | }, 240 | }, 241 | }, 242 | }, 243 | }, 244 | expectedMemory: []interface{}{ 245 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("done"), // start 246 | }, 247 | expectedOutput: "done\n", 248 | }, 249 | "InlineUnlessFalse": { 250 | program: &CompiledProgram{ 251 | Functions: map[string]*CompiledFunction{ 252 | "start": { 253 | Variables: []interface{}{ 254 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), 255 | }, 256 | Instructions: []Instruction{ 257 | &ConditionJumpInstruction{ 258 | Left: 0, 259 | Right: 1, 260 | Operator: OperatorEqual, 261 | True: 2, 262 | False: 1, 263 | }, 264 | &CallInstruction{ 265 | Call: "display ?", 266 | Args: []int{2}, 267 | }, 268 | &CallInstruction{ 269 | Call: "display ?", 270 | Args: []int{3}, 271 | }, 272 | }, 273 | }, 274 | }, 275 | }, 276 | expectedMemory: []interface{}{ 277 | NewText("foo"), NewText("bar"), NewText("match!"), NewText("done"), // start 278 | }, 279 | expectedOutput: "match!\ndone\n", 280 | }, 281 | "InlineUnlessElseTrue": { 282 | program: &CompiledProgram{ 283 | Functions: map[string]*CompiledFunction{ 284 | "start": { 285 | Variables: []interface{}{ 286 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("no match!"), NewText("done"), 287 | }, 288 | Instructions: []Instruction{ 289 | &ConditionJumpInstruction{ 290 | Left: 0, 291 | Right: 1, 292 | Operator: OperatorEqual, 293 | True: 2, 294 | False: 1, 295 | }, 296 | &CallInstruction{ 297 | Call: "display ?", 298 | Args: []int{2}, 299 | }, 300 | &JumpInstruction{ 301 | Forward: 2, 302 | }, 303 | &CallInstruction{ 304 | Call: "display ?", 305 | Args: []int{3}, 306 | }, 307 | &CallInstruction{ 308 | Call: "display ?", 309 | Args: []int{4}, 310 | }, 311 | }, 312 | }, 313 | }, 314 | }, 315 | expectedMemory: []interface{}{ 316 | NewText("foo"), NewText("foo"), NewText("match!"), NewText("no match!"), NewText("done"), // start 317 | }, 318 | expectedOutput: "done\n", 319 | }, 320 | "InlineWhile": { 321 | program: &CompiledProgram{ 322 | Functions: map[string]*CompiledFunction{ 323 | "start": { 324 | Variables: []interface{}{ 325 | NewNumber("0", 6), NewNumber("5", 6), NewNumber("1", 6), NewText("done"), 326 | }, 327 | Instructions: []Instruction{ 328 | &ConditionJumpInstruction{ 329 | Left: 0, 330 | Right: 1, 331 | Operator: OperatorLessThan, 332 | True: 1, 333 | False: 3, 334 | }, 335 | &CallInstruction{ 336 | Call: "add ? and ? into ?", 337 | Args: []int{0, 2, 0}, 338 | }, 339 | &JumpInstruction{ 340 | Forward: -2, 341 | }, 342 | &CallInstruction{ 343 | Call: "display ?", 344 | Args: []int{3}, 345 | }, 346 | }, 347 | }, 348 | }, 349 | }, 350 | expectedMemory: []interface{}{ 351 | NewNumber("5", 6), NewNumber("5", 6), NewNumber("1", 6), NewText("done"), // start 352 | }, 353 | expectedOutput: "done\n", 354 | }, 355 | "InlineUntil": { 356 | program: &CompiledProgram{ 357 | Functions: map[string]*CompiledFunction{ 358 | "start": { 359 | Variables: []interface{}{ 360 | NewNumber("0", 6), NewNumber("5", 6), NewNumber("1", 6), NewText("done"), 361 | }, 362 | Instructions: []Instruction{ 363 | &ConditionJumpInstruction{ 364 | Left: 0, 365 | Right: 1, 366 | Operator: OperatorGreaterThan, 367 | True: 3, 368 | False: 1, 369 | }, 370 | &CallInstruction{ 371 | Call: "add ? and ? into ?", 372 | Args: []int{0, 2, 0}, 373 | }, 374 | &JumpInstruction{ 375 | Forward: -2, 376 | }, 377 | &CallInstruction{ 378 | Call: "display ?", 379 | Args: []int{3}, 380 | }, 381 | }, 382 | }, 383 | }, 384 | }, 385 | expectedMemory: []interface{}{ 386 | NewNumber("6", 6), NewNumber("5", 6), NewNumber("1", 6), NewText("done"), // start 387 | }, 388 | expectedOutput: "done\n", 389 | }, 390 | "SetNumberRequiresRounding": { 391 | program: &CompiledProgram{ 392 | Functions: map[string]*CompiledFunction{ 393 | "start": { 394 | Variables: []interface{}{ 395 | NewNumber("0", 1), NewNumber("1.23", 6), 396 | }, 397 | Instructions: []Instruction{ 398 | &CallInstruction{ 399 | Call: "set ? to ?", 400 | Args: []int{0, 1}, 401 | }, 402 | }, 403 | }, 404 | }, 405 | }, 406 | expectedMemory: []interface{}{ 407 | NewNumber("1.2", 1), NewNumber("1.23", 6), // start 408 | }, 409 | }, 410 | "DisplayBlackhole": { 411 | program: &CompiledProgram{ 412 | Functions: map[string]*CompiledFunction{ 413 | "start": { 414 | Instructions: []Instruction{ 415 | &CallInstruction{ 416 | Call: "display ?", 417 | Args: []int{blackholeVariableIndex}, 418 | }, 419 | }, 420 | }, 421 | }, 422 | }, 423 | expectedOutput: "\n", 424 | }, 425 | "SetBlackhole": { 426 | program: &CompiledProgram{ 427 | Functions: map[string]*CompiledFunction{ 428 | "start": { 429 | Variables: []interface{}{ 430 | NewNumber("123", 6), 431 | }, 432 | Instructions: []Instruction{ 433 | &CallInstruction{ 434 | Call: "set ? to ?", 435 | Args: []int{0, blackholeVariableIndex}, 436 | }, 437 | }, 438 | }, 439 | }, 440 | }, 441 | expectedMemory: []interface{}{ 442 | NewNumber("123", 6), // start 443 | }, 444 | expectedOutput: "", 445 | }, 446 | "IfQuestionYes": { 447 | program: &CompiledProgram{ 448 | Functions: map[string]*CompiledFunction{ 449 | "start": { 450 | Variables: []interface{}{ 451 | NewText("good"), 452 | }, 453 | Instructions: []Instruction{ 454 | &CallInstruction{ 455 | Call: "something is true", 456 | }, 457 | &QuestionJumpInstruction{ 458 | True: 1, 459 | False: 2, 460 | }, 461 | &CallInstruction{ 462 | Call: "display ?", 463 | Args: []int{0}, 464 | }, 465 | }, 466 | }, 467 | "something is true": { 468 | Instructions: []Instruction{ 469 | &QuestionAnswerInstruction{ 470 | Yes: true, 471 | }, 472 | }, 473 | }, 474 | }, 475 | }, 476 | expectedMemory: []interface{}{ 477 | NewText("good"), // start 478 | // something is true (nothing) 479 | }, 480 | expectedOutput: "good\n", 481 | }, 482 | "IfQuestionNo": { 483 | program: &CompiledProgram{ 484 | Functions: map[string]*CompiledFunction{ 485 | "start": { 486 | Variables: []interface{}{ 487 | NewText("good"), 488 | }, 489 | Instructions: []Instruction{ 490 | &CallInstruction{ 491 | Call: "something is true", 492 | }, 493 | &QuestionJumpInstruction{ 494 | True: 1, 495 | False: 2, 496 | }, 497 | &CallInstruction{ 498 | Call: "display ?", 499 | Args: []int{0}, 500 | }, 501 | }, 502 | }, 503 | "something is true": { 504 | Instructions: []Instruction{ 505 | &QuestionAnswerInstruction{ 506 | Yes: false, 507 | }, 508 | }, 509 | }, 510 | }, 511 | }, 512 | expectedMemory: []interface{}{ 513 | NewText("good"), // start 514 | // something is true (nothing) 515 | }, 516 | expectedOutput: "", 517 | }, 518 | "IfQuestionMissingAnswerIsNo": { 519 | program: &CompiledProgram{ 520 | Functions: map[string]*CompiledFunction{ 521 | "start": { 522 | Variables: []interface{}{ 523 | NewText("good"), 524 | }, 525 | Instructions: []Instruction{ 526 | &CallInstruction{ 527 | Call: "something is true", 528 | }, 529 | &QuestionJumpInstruction{ 530 | True: 1, 531 | False: 2, 532 | }, 533 | &CallInstruction{ 534 | Call: "display ?", 535 | Args: []int{0}, 536 | }, 537 | }, 538 | }, 539 | "something is true": {}, 540 | }, 541 | }, 542 | expectedMemory: []interface{}{ 543 | NewText("good"), // start 544 | // something is true (nothing) 545 | }, 546 | expectedOutput: "", 547 | }, 548 | } 549 | 550 | var vmConditionTests = map[string]interface{}{ 551 | `"foo" = "foo"`: true, // text 552 | `"foo" = "bar"`: false, 553 | `1.230 = 1.23`: true, // number 554 | `1.23 = 2.23`: false, 555 | `1.23 = "1.23"`: "cannot compare: number = text", // mixed 556 | `"1.23" = 1.23`: "cannot compare: text = number", 557 | 558 | `"foo" != "foo"`: false, // text 559 | `"foo" != "bar"`: true, 560 | `1.230 != 1.23`: false, // number 561 | `1.23 != 2.23`: true, 562 | `1.23 != "1.23"`: "cannot compare: number != text", // mixed 563 | `"1.23" != 1.23`: "cannot compare: text != number", 564 | 565 | `"foo" < "foo"`: false, // text 566 | `"foo" < "bar"`: false, 567 | `1.230 < 1.23`: false, // number 568 | `1.23 < 2.23`: true, 569 | `1.23 < "1.23"`: "cannot compare: number < text", // mixed 570 | `"1.23" < 1.23`: "cannot compare: text < number", 571 | 572 | `"foo" <= "foo"`: true, // text 573 | `"foo" <= "bar"`: false, 574 | `1.230 <= 1.23`: true, // number 575 | `1.23 <= 2.23`: true, 576 | `1.23 <= "1.23"`: "cannot compare: number <= text", // mixed 577 | `"1.23" <= 1.23`: "cannot compare: text <= number", 578 | 579 | `"foo" > "foo"`: false, // text 580 | `"foo" > "bar"`: true, 581 | `1.230 > 1.23`: false, // number 582 | `1.23 > 2.23`: false, 583 | `1.23 > "1.23"`: "cannot compare: number > text", // mixed 584 | `"1.23" > 1.23`: "cannot compare: text > number", 585 | 586 | `"foo" >= "foo"`: true, // text 587 | `"foo" >= "bar"`: true, 588 | `1.230 >= 1.23`: true, // number 589 | `1.23 >= 2.23`: false, 590 | `1.23 >= "1.23"`: "cannot compare: number >= text", // mixed 591 | `"1.23" >= 1.23`: "cannot compare: text >= number", 592 | } 593 | 594 | func TestVirtualMachine_Run(t *testing.T) { 595 | for testName, test := range vmTests { 596 | t.Run(testName, func(t *testing.T) { 597 | vm := NewVirtualMachine(test.program) 598 | vm.out = bytes.NewBuffer(nil) 599 | 600 | err := vm.Run() 601 | require.NoError(t, err) 602 | 603 | assert.Equal(t, test.expectedMemory, vm.memory) 604 | assert.Equal(t, test.expectedOutput, vm.out.(*bytes.Buffer).String()) 605 | }) 606 | } 607 | } 608 | 609 | func TestVirtualMachine_ConditionTests(t *testing.T) { 610 | for test, expected := range vmConditionTests { 611 | t.Run(test, func(t *testing.T) { 612 | parser := NewParser(strings.NewReader( 613 | "start: if " + test + ", display \"yes\"", 614 | )) 615 | program, err := parser.Parse() 616 | require.NoError(t, err) 617 | 618 | compiler := NewCompiler(program) 619 | compiledProgram := compiler.Compile() 620 | 621 | vm := NewVirtualMachine(compiledProgram) 622 | vm.out = bytes.NewBuffer(nil) 623 | err = vm.Run() 624 | 625 | switch expected { 626 | case true: 627 | assert.Equal(t, "yes\n", vm.out.(*bytes.Buffer).String()) 628 | assert.NoError(t, err) 629 | 630 | case false: 631 | assert.Equal(t, "", vm.out.(*bytes.Buffer).String()) 632 | assert.NoError(t, err) 633 | 634 | default: 635 | assert.EqualError(t, err, expected.(string)) 636 | } 637 | }) 638 | } 639 | } 640 | --------------------------------------------------------------------------------