├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── docker ├── dockerfile └── makefile ├── examples ├── bash pitfalls │ ├── array.sh │ ├── dict.sh │ ├── floating-point.sh │ └── readme.md ├── hush vs lua │ └── shell capabilities.org └── hush │ ├── heap.hsh │ └── iterator.hsh ├── images └── logo.png ├── readme.org ├── release.org ├── rustfmt.toml ├── src ├── args.rs ├── fmt.rs ├── io.rs ├── main.rs ├── runtime │ ├── command │ │ ├── arg.rs │ │ ├── exec │ │ │ ├── error.rs │ │ │ ├── fmt.rs │ │ │ ├── join.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── flow.rs │ ├── lib.rs │ ├── lib │ │ ├── args.rs │ │ ├── assert.rs │ │ ├── base64.rs │ │ ├── bind.rs │ │ ├── bytes.rs │ │ ├── catch.rs │ │ ├── cd.rs │ │ ├── contains.rs │ │ ├── cwd.rs │ │ ├── env.rs │ │ ├── error.rs │ │ ├── exit.rs │ │ ├── float.rs │ │ ├── glob.rs │ │ ├── has_error.rs │ │ ├── hex.rs │ │ ├── import.rs │ │ ├── int.rs │ │ ├── is_empty.rs │ │ ├── iter.rs │ │ ├── json.rs │ │ ├── length.rs │ │ ├── panic.rs │ │ ├── pop.rs │ │ ├── print.rs │ │ ├── println.rs │ │ ├── push.rs │ │ ├── rand.rs │ │ ├── range.rs │ │ ├── read.rs │ │ ├── regex.rs │ │ ├── replace.rs │ │ ├── sleep.rs │ │ ├── sort.rs │ │ ├── split.rs │ │ ├── substr.rs │ │ ├── to_string.rs │ │ ├── trim.rs │ │ ├── type_.rs │ │ ├── typecheck.rs │ │ └── util.rs │ ├── mem.rs │ ├── mod.rs │ ├── panic.rs │ ├── source.rs │ ├── tests │ │ ├── data │ │ │ ├── negative │ │ │ │ ├── asserts │ │ │ │ │ ├── assert.hsh │ │ │ │ │ ├── closures.hsh │ │ │ │ │ ├── iter-fun.hsh │ │ │ │ │ ├── self.hsh │ │ │ │ │ └── type.hsh │ │ │ │ ├── assign-error-field.hsh │ │ │ │ └── compare-int-float.hsh │ │ │ ├── positive │ │ │ │ ├── assert.hsh │ │ │ │ ├── bind.hsh │ │ │ │ ├── capture-large.hsh │ │ │ │ ├── capture.hsh │ │ │ │ ├── catch.hsh │ │ │ │ ├── closures.hsh │ │ │ │ ├── command-bail.hsh │ │ │ │ ├── contains.hsh │ │ │ │ ├── dict-iter.hsh │ │ │ │ ├── env.hsh │ │ │ │ ├── factorial.hsh │ │ │ │ ├── float.hsh │ │ │ │ ├── int-float-hash.hsh │ │ │ │ ├── int.hsh │ │ │ │ ├── iter-fun.hsh │ │ │ │ ├── iter-lib.hsh │ │ │ │ ├── pipeline.hsh │ │ │ │ ├── print.hsh │ │ │ │ ├── redirections.hsh │ │ │ │ ├── self.hsh │ │ │ │ ├── sort.hsh │ │ │ │ ├── split.hsh │ │ │ │ ├── try.hsh │ │ │ │ ├── type.hsh │ │ │ │ └── typecheck.hsh │ │ │ └── stdout-stderr.sh │ │ └── mod.rs │ └── value │ │ ├── array.rs │ │ ├── dict.rs │ │ ├── error.rs │ │ ├── errors.rs │ │ ├── float.rs │ │ ├── fmt.rs │ │ ├── function.rs │ │ ├── mod.rs │ │ ├── ops.rs │ │ └── string.rs ├── semantic │ ├── error │ │ ├── fmt.rs │ │ └── mod.rs │ ├── mod.rs │ ├── program │ │ ├── command.rs │ │ ├── fmt.rs │ │ ├── mem │ │ │ ├── fmt.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── scope.rs │ └── tests │ │ ├── data │ │ ├── negative │ │ │ ├── async-builtin-1.hsh │ │ │ ├── async-builtin-2.hsh │ │ │ ├── async-builtin-3.hsh │ │ │ ├── break-outside-loop-1.hsh │ │ │ ├── break-outside-loop-2.hsh │ │ │ ├── duplicate-dict-keys-1.hsh │ │ │ ├── invalid-assignment-1.hsh │ │ │ ├── invalid-assignment-2.hsh │ │ │ ├── return-outside-function-1.hsh │ │ │ ├── try-outside-function.hsh │ │ │ ├── undeclared-variable-1.hsh │ │ │ ├── undeclared-variable-2.hsh │ │ │ └── undeclared-variable-3.hsh │ │ └── positive │ │ │ ├── closures.hsh │ │ │ ├── scope.hsh │ │ │ └── try.hsh │ │ └── mod.rs ├── symbol │ ├── fmt.rs │ └── mod.rs ├── syntax │ ├── ast │ │ ├── command.rs │ │ ├── fmt.rs │ │ └── mod.rs │ ├── error │ │ ├── fmt.rs │ │ └── mod.rs │ ├── fmt.rs │ ├── lexer │ │ ├── automata │ │ │ ├── argument.rs │ │ │ ├── command.rs │ │ │ ├── comment.rs │ │ │ ├── expansion.rs │ │ │ ├── mod.rs │ │ │ ├── number.rs │ │ │ ├── root.rs │ │ │ ├── string.rs │ │ │ ├── symbol.rs │ │ │ └── word.rs │ │ ├── cursor.rs │ │ ├── error │ │ │ ├── fmt.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── tests.rs │ │ └── token │ │ │ ├── fmt.rs │ │ │ └── mod.rs │ ├── mod.rs │ ├── parser │ │ ├── command.rs │ │ ├── error │ │ │ ├── fmt.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── sync.rs │ ├── source.rs │ └── tests │ │ ├── data │ │ ├── negative │ │ │ ├── command-block-1.hsh │ │ │ ├── command-block-2.hsh │ │ │ ├── command-block-3.hsh │ │ │ ├── conditionals-1.hsh │ │ │ ├── conditionals-2.hsh │ │ │ ├── dollar-1.hsh │ │ │ ├── dollar-2.hsh │ │ │ ├── dollar-3.hsh │ │ │ ├── expr-1.hsh │ │ │ ├── expr-2.hsh │ │ │ ├── expr-3.hsh │ │ │ ├── for-loop-1.hsh │ │ │ ├── for-loop-is-not-an-expression.hsh │ │ │ ├── funcall-1.hsh │ │ │ ├── funcall-2.hsh │ │ │ ├── statement-1.hsh │ │ │ ├── while-loop-1.hsh │ │ │ └── while-loop-is-not-an-expression.hsh │ │ └── positive │ │ │ ├── command-block.hsh │ │ │ ├── conditionals.hsh │ │ │ ├── funcall.hsh │ │ │ ├── keywords.hsh │ │ │ ├── literals │ │ │ ├── arrays.hsh │ │ │ ├── basic.hsh │ │ │ ├── chars.hsh │ │ │ ├── dicts.hsh │ │ │ ├── functions.hsh │ │ │ └── strings.hsh │ │ │ ├── operators.hsh │ │ │ └── variables.hsh │ │ └── mod.rs ├── term │ ├── color.rs │ └── mod.rs └── tests │ ├── mod.rs │ └── util.rs └── syntax-highlight ├── emacs └── hush-mode.el ├── pygments ├── lexer │ ├── __init__.py │ └── hush.py ├── readme.org └── setup.py ├── vis ├── README.md ├── hush.lua └── hush_detect.lua └── vscode ├── .vscode └── launch.json ├── language-configuration.json ├── package.json ├── syntaxes └── hush.tmLanguage.json └── vsc-extension-quickstart.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /docker/bin 3 | 4 | /syntax-highlight/pygments/build/ 5 | /syntax-highlight/pygments/dist/ 6 | /syntax-highlight/pygments/hushlexer.egg-info/ 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hush" 3 | description = "Hush is a unix shell scripting language based on the Lua programming language" 4 | version = "0.1.4" 5 | authors = ["gahag "] 6 | edition = "2018" 7 | homepage = "https://github.com/gahag/hush" 8 | repository = "https://github.com/gahag/hush" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | automod = "1.0" 13 | 14 | clap = "2.33" 15 | termion = "1.5" 16 | 17 | intaglio = "1.2" 18 | gc = { version = "0.4", features = ["derive"] } 19 | regex = { version = "1.5", default-features = false, features = [ "std", "unicode-perl" ] } 20 | os_pipe = "1.0" 21 | inventory = "0.1" 22 | bstr = "0.2" 23 | glob = "0.3" 24 | 25 | serial_test = "0.5" 26 | 27 | serde = "1.0" 28 | serde_json = "1.0" 29 | base64 = "0.13" 30 | hex = "0.4" 31 | rand = "0.8.5" 32 | rand_chacha = "0.3.1" 33 | 34 | [dev-dependencies] 35 | assert_matches = "1.5" 36 | 37 | [profile.release] 38 | lto = true 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021-2022 Gabriel Bastos 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /docker/dockerfile: -------------------------------------------------------------------------------- 1 | from rust:alpine 2 | 3 | run apk add musl-dev 4 | 5 | run mkdir /hush 6 | 7 | volume /hush 8 | 9 | cmd cargo install hush --root /hush 10 | -------------------------------------------------------------------------------- /docker/makefile: -------------------------------------------------------------------------------- 1 | image: 2 | @docker build . -t hush-builder 3 | 4 | build: 5 | @docker run --rm -v $(shell pwd)/:/hush hush-builder 6 | -------------------------------------------------------------------------------- /examples/bash pitfalls/array.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | # Consider a file containing measurements of any given parameter of a machine learning 5 | # model in production. Most values occur in the range 0-10, but some values get quite 6 | # larger. One might want to generate a notification if the average of such values exceeds 7 | # some threshold. That is, the average of the values higher than 10 is higher than X. 8 | data=$(cat <<'END_HEREDOC' 9 | 16.47 10 | 8.7 11 | 8.4 12 | 4.22 13 | 14.96 14 | 71.56 15 | 1.73 16 | 71.56 17 | 2.4 18 | 208.09 19 | 7.2 20 | 0.09 21 | 2.6 22 | 205.39 23 | 1.6 24 | 1.0 25 | 3.1 26 | END_HEREDOC 27 | ) 28 | 29 | # In pure bash, this is practically impossible to do. A solution using awk is quite 30 | # feasible, but awk itself is just another programming language like python or perl. With 31 | # awk, one can't easily call external programs, which in more sophisticated scenarios, can 32 | # be required as an intermediate step in the calculation. 33 | 34 | echo "$data" \ 35 | | awk '{ if ($1 > 10.0) { sum += $1; count++; } } END { print sum/count }' 36 | -------------------------------------------------------------------------------- /examples/bash pitfalls/dict.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | # Consider the scenario where we have a bunch of files, and we want to copy them to some 5 | # servers based on a given property. The associative array below (requires bash >= 4.0) 6 | # describes to which server each kind of file must go. 7 | 8 | declare -A target_addresses=( 9 | # Here we have to hack the remote addresses as a single string of space separated items. 10 | ['100']='10.0.0.1:/data/ 10.0.0.2:/data/ 10.0.0.8:/data/' 11 | ['200']='10.0.0.3:/data/ 10.0.0.4:/data/' 12 | ['300']='10.0.0.5:/data/ 10.0.0.6:/data/' 13 | ['400']='10.0.0.7:/data/ 10.0.0.8:/data/' 14 | ['500']='10.0.0.9:/data/ 10.0.0.10:/data/ 10.0.0.8:/data/' 15 | ['600']='10.0.0.1:/data/ 10.0.0.2:/data/' 16 | ['700']='10.0.0.3:/data/ 10.0.0.4:/data/' 17 | ['800']='10.0.0.5:/data/ 10.0.0.6:/data/' 18 | ['900']='10.0.0.7:/data/ 10.0.0.8:/data/ 10.0.0.8:/data/' 19 | ) 20 | 21 | 22 | for input_file in ./*; do 23 | key=$(echo "$input_file" | cut -d '_' -f 2) # Extract the key from the input file name. 24 | 25 | # Here, we leverage the shell's standard word splitting on spaces. If any of the target 26 | # directories' name contains spaces, then this would break. We would then have to use 27 | # another character as separator, and manipulate the IFS variable accordingly. In fact, 28 | # there is no character we can use that would work in every single case, as unix 29 | # filenames may contain everything but the null character. Furthermore, if the paths 30 | # contain asterisks, glob expansion takes place. 31 | for remote_address in ${target_addresses[$key]}; do 32 | echo $input_file $remote_address 33 | done 34 | done 35 | -------------------------------------------------------------------------------- /examples/bash pitfalls/floating-point.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | # Ceph is a distributed file system, which provides the `ceph df` command for inspecting 5 | # space usage. An example of such command's output follows: 6 | ceph_df_out=$(cat <<'END_HEREDOC' 7 | RAW STORAGE: 8 | CLASS SIZE AVAIL USED RAW USED %RAW USED 9 | hdd 62 TiB 52 TiB 10 TiB 10 TiB 16.47 10 | ssd 8.7 TiB 8.4 TiB 370 GiB 377 GiB 4.22 11 | TOTAL 71 TiB 60 TiB 11 TiB 11 TiB 14.96 12 | 13 | POOLS: 14 | POOL ID STORED OBJECTS USED %USED MAX AVAIL QUOTA OBJECTS QUOTA BYTES DIRTY USED COMPR UNDER COMPR 15 | rbd-kubernetes 36 288 GiB 71.56k 865 GiB 1.73 16 TiB N/A N/A 71.56k 0 B 0 B 16 | rbd-cache 41 2.4 GiB 208.09k 7.2 GiB 0.09 2.6 TiB N/A N/A 205.39k 0 B 0 B 17 | cephfs-metadata 51 529 MiB 221 1.6 GiB 0 16 TiB N/A N/A 221 0 B 0 B 18 | cephfs-data 52 1.0 GiB 424 3.1 GiB 0 16 TiB N/A N/A 424 0 B 0 B 19 | END_HEREDOC 20 | ) 21 | # One might want to have a script check if any of the pools has exceeded some usage 22 | # percentage threshold. That is, for a threshold T, the script should check if there is 23 | # any line where the %USED column exceeds T. One possible implementation of such script 24 | # follows. 25 | 26 | threshold=80.0 27 | 28 | usages=$( 29 | echo "$ceph_df_out" \ 30 | | sed '0,/^POOLS:$/d' \ 31 | | tail -n +2 \ 32 | | tr -s ' ' \ 33 | | cut -d ' ' -f9 34 | # Well, this is not great to start with. The sed(1) line can be a little cryptic, and we 35 | # have to work around the remaining lime with tail(1). 36 | ) 37 | 38 | for usage in $usages; do 39 | # Here comes one of the main issues. We can't work with floats in bash. We must resort 40 | # to bc(1) or some other alternative. And how do we get the result back? Well, bc(1) 41 | # outputs `0` or `1` for relational operators. We must then convert such value to 42 | # boolean, using the numeric context `(( ))`. That's a lot for what should be simply 43 | # `$usage > $threshold`, and it introduces a whole myriad of possible issues of its own. 44 | if (( $(echo "$usage > $threshold" | bc) )); then 45 | # Emit a warning. Maybe we could send an email here. 46 | echo "Pool usage above threshold!!!" 47 | fi 48 | # Furthermore, it doesn't scale. What if we wanted to sum a given column? Things get out 49 | # of hand pretty quickly. 50 | done 51 | -------------------------------------------------------------------------------- /examples/bash pitfalls/readme.md: -------------------------------------------------------------------------------- 1 | # Bash Pitfalls Examples 2 | 3 | In this directory, you will find some snippets reduced from real examples of scripts where 4 | the lack of standard data capabilities incurred in hacky, inflexible or unscalable 5 | solutions. These are examples that show where the shell could be more flexible and 6 | elaborated, without necessarily being more complex. 7 | -------------------------------------------------------------------------------- /examples/hush vs lua/shell capabilities.org: -------------------------------------------------------------------------------- 1 | * Interaction with commands 2 | Lua provides APIs for simple interactions with external programs. The two main functions 3 | for invoking commands are ~os.execute~, which executes its argument as a shell command, 4 | and ~io.popen~, which does the same, but pipes the input *or* the output. 5 | 6 | A minor issue present in both functions is that they receive a single string as 7 | input. Therefore, one must build such string programatically in order to include values 8 | from variables, and take care to prevent unintentional word splitting or globbing from 9 | the shell. Further on, to read the output when using ~io.popen~, one must do it verbosely 10 | using the ~read~ and ~close~ methods. 11 | 12 | Lua: 13 | #+begin_src lua 14 | function restartContainerIfRunning(container_name) 15 | local docker_inspect = io.popen('docker inspect "' .. container_name .. '"') 16 | 17 | local output = docker_inspect:read('*a') 18 | 19 | if docker_inspect:close() == false then 20 | -- docker inspect failed, return error 21 | end 22 | 23 | local container_info = json.parse(output)[0] 24 | 25 | if container_info.State.Running then 26 | return os.execute('docker restart ' .. container_name) 27 | end 28 | end 29 | #+end_src 30 | 31 | Hush: 32 | #+begin_src lua 33 | function restartContainerIfRunning(container_name) 34 | docker_inspect = ${ docker inspect $container_name } 35 | 36 | if docker_inspect.status != 0 then 37 | -- docker inspect failed, return error 38 | end 39 | 40 | container_info = json.parse(docker_inspect.stdout)[0] 41 | 42 | if container_info.State.Running then 43 | return { docker restart $container_name } 44 | end 45 | end 46 | #+end_src 47 | 48 | But it gets worse: ~io.popen~ provides a monodirectional pipe, which means 49 | you can't send input and capture the output at the same time. 50 | 51 | Hush: 52 | #+begin_src lua 53 | function list_music_files() 54 | return { 55 | find '/media/musics' 56 | -iname '*.mp3'; 57 | 58 | cat /org/musics_to_be_downloaded.org 59 | | awk -F'TODO' '{print $NF}' # Get everything after the last TODO 60 | } 61 | end 62 | 63 | -- The following statement calls a Hush function, pipes its output to a command block, and 64 | -- captures the resulting output. Such task in almost impossible in Lua, because in the 65 | -- current io.popen API reading and writing are mutually exclusive, and there is no simple 66 | -- way of capturing the output of isolated Lua functions. 67 | 68 | my_musics = (list_music_files() | ${ sort | uniq }).stdout 69 | #+end_src 70 | 71 | Workarounds for such limitations are possible in Lua, but are much more complex than 72 | they could be. For instance, one can implement a more flexible version of ~io.popen~ as 73 | follows: 74 | #+begin_src lua 75 | require("posix") 76 | 77 | -- 78 | -- popen3() implementation, from https://stackoverflow.com/a/16515126 79 | -- 80 | function popen3(path, ...) 81 | local r1, w1 = posix.pipe() 82 | local r2, w2 = posix.pipe() 83 | local r3, w3 = posix.pipe() 84 | 85 | assert((w1 ~= nil or r2 ~= nil or r3 ~= nil), "pipe() failed") 86 | 87 | local pid, err = posix.fork() 88 | assert(pid ~= nil, "fork() failed") 89 | if pid == 0 then 90 | posix.close(w1) 91 | posix.close(r2) 92 | posix.dup2(r1, posix.fileno(io.stdin)) 93 | posix.dup2(w2, posix.fileno(io.stdout)) 94 | posix.dup2(w3, posix.fileno(io.stderr)) 95 | posix.close(r1) 96 | posix.close(w2) 97 | posix.close(w3) 98 | 99 | local ret, err = posix.execp(path, unpack({...})) 100 | assert(ret ~= nil, "execp() failed") 101 | 102 | posix._exit(1) 103 | else 104 | posix.close(r1) 105 | posix.close(w2) 106 | posix.close(w3) 107 | 108 | return pid, w1, r2, r3 109 | end 110 | end 111 | #+end_src 112 | 113 | To allow piping into or capturing the output of shell functions, further calls to ~dup2~ 114 | would be necessary to temporarily pipe the shell's IO streams. 115 | -------------------------------------------------------------------------------- /examples/hush/heap.hsh: -------------------------------------------------------------------------------- 1 | # Heap: A simple binary heap. 2 | 3 | # Construct a heap in the given array. 4 | # Parameters: 5 | # * array: the data array 6 | # * cmp: function or nil -- the comparison function, defaults to ascending 7 | # Returns the heap instance. 8 | function(array, cmp) 9 | if cmp == nil then 10 | cmp = function (a, b) 11 | a < b 12 | end 13 | end 14 | 15 | let heap = @[ 16 | _data: array, 17 | _cmp: cmp, 18 | 19 | # Swap two elements in the array. 20 | _swap: function(ix1, ix2) 21 | let tmp = self._data[ix1] 22 | self._data[ix1] = self._data[ix2] 23 | self._data[ix2] = tmp 24 | end, 25 | 26 | # Recursively update a node up in the array. 27 | _percolate_up: function (index) 28 | let parent = index / 2 29 | 30 | if index == 0 or self._cmp(self._data[parent], self._data[index]) then 31 | return 32 | end 33 | 34 | self._swap(index, parent) 35 | 36 | self._percolate_up(parent) 37 | end, 38 | 39 | # Recursively update a node down in the array. 40 | _percolate_down: function (index) 41 | let size = std.len(self._data) 42 | let max = 0 43 | let left = 2 * index 44 | let right = left + 1 45 | 46 | max = if (left < size) and self._cmp(self._data[left], self._data[index]) then 47 | left 48 | else 49 | index 50 | end 51 | 52 | if (right < size) and self._cmp(self._data[right], self._data[max]) then 53 | max = right 54 | end 55 | 56 | if max == index then 57 | return 58 | end 59 | 60 | self._swap(index, max) 61 | 62 | self._percolate_down(max) 63 | end, 64 | 65 | # Get the size of the collection. 66 | size: function () 67 | std.len(self._data) 68 | end, 69 | 70 | # Returns whether the collection is empty. 71 | is_empty: function () 72 | self.size() == 0 73 | end, 74 | 75 | # Empty the collection. 76 | clear: function () 77 | self._data = [] 78 | end, 79 | 80 | # Peek the top element. 81 | # Returns the top element. 82 | peek: function () 83 | self._data[0] 84 | end, 85 | 86 | # Pop the top element. 87 | # This function updates the heap after extracting the top element. 88 | # Returns the top element. 89 | pop: function () 90 | let elem = self._data[0] 91 | 92 | self._data[0] = self._data[self.size() - 1] 93 | 94 | std.pop(self._data) 95 | 96 | if not self.is_empty() then 97 | self._percolate_down(0) 98 | end 99 | 100 | elem 101 | end, 102 | 103 | # Push an element into the heap. 104 | push: function (elem) 105 | std.push(self._data, elem) 106 | 107 | self._percolate_up(self.size() - 1) 108 | end, 109 | ] 110 | 111 | for i in std.range(heap.size() / 2, -1, -1) do 112 | heap._percolate_down(i) 113 | end 114 | 115 | heap 116 | end 117 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hush-shell/hush/560c33a2dc8bf967b4fb0c80e3f18ca8934615ff/images/logo.png -------------------------------------------------------------------------------- /readme.org: -------------------------------------------------------------------------------- 1 | #+html: Logo 2 | 3 | * Hush 4 | /Hush/ is a /Unix/ shell scripting language inspired by the /[[http://www.lua.org/][Lua/ programming 5 | language]]. 6 | 7 | Check the [[https://hush-shell.github.io][homepage]] for more details. 8 | -------------------------------------------------------------------------------- /release.org: -------------------------------------------------------------------------------- 1 | * TODO Bump version 2 | [[file:Cargo.toml][Cargo.toml]] 3 | * TODO Publish to crates.io 4 | #+begin_src run 5 | cargo publish 6 | #+end_src 7 | * TODO Generate AUR artifacts 8 | #+begin_src run 9 | cargo aur 10 | #+end_src 11 | * TODO Generate static binary 12 | #+begin_src run 13 | cd docker; make build 14 | #+end_src 15 | * TODO Create github release with both Arch Linux and static binary 16 | We must create both, because the AUR package will install the Arch Linux one. 17 | * TODO Move =PKGBUILD= to aur repository 18 | Remember to discard the URL change, and keep only the sha hash change. 19 | * TODO Update srcinfo 20 | : makepkg --printsrcinfo > .SRCINFO 21 | * TODO Commit and push AUR package 22 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | blank_lines_upper_bound = 2 3 | struct_lit_width = 50 4 | spaces_around_ranges = true 5 | imports_layout = "HorizontalVertical" 6 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::{OsStr, OsString}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}}; 2 | 3 | use clap::{AppSettings, clap_app, crate_authors, crate_description, crate_version}; 4 | 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 7 | pub enum Command { 8 | Help(Box), 9 | Version(Box), 10 | Run(Args) 11 | } 12 | 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 15 | pub struct Args { 16 | pub script_path: Option, 17 | /// Check program with static analysis, but don't run. 18 | pub check: bool, 19 | /// Print the lexemes. 20 | pub print_lexemes: bool, 21 | /// Print the AST. 22 | pub print_ast: bool, 23 | /// Print the program. 24 | pub print_program: bool, 25 | /// Arguments for the script. 26 | pub script_args: Box<[Box<[u8]>]> 27 | } 28 | 29 | 30 | pub fn parse(args: A) -> clap::Result 31 | where 32 | A: IntoIterator, 33 | T: Into + Clone 34 | { 35 | let app = 36 | clap_app!( 37 | Hush => 38 | (version: crate_version!()) 39 | (author: crate_authors!()) 40 | (about: crate_description!()) 41 | (@arg check: --check "Perform only static analysis instead of executing.") 42 | (@arg lex: --lex "Print the lexemes") 43 | (@arg ast: --ast "Print the AST") 44 | (@arg program: --program "Print the PROGAM") 45 | // The script path must not be a separate parameter because we must prevent clap 46 | // from parsing flags to the right of the script path. 47 | (@arg arguments: ... +allow_hyphen_values "Script and/or arguments") 48 | ) 49 | .setting(AppSettings::TrailingVarArg); 50 | 51 | match app.get_matches_from_safe(args) { 52 | Ok(matches) => { 53 | let mut arguments = matches 54 | .values_of_os("arguments") 55 | .into_iter() 56 | .flatten() 57 | .map(OsStrExt::as_bytes); 58 | 59 | let mut script_args = Vec::new(); 60 | let script_path = match arguments.next() { 61 | None => None, 62 | Some(b"-") => None, 63 | Some(arg) => { 64 | let path = Path::new(OsStr::from_bytes(arg)); 65 | if path.is_file() { 66 | Some(path.to_owned()) 67 | } else { 68 | script_args.push(arg.into()); 69 | None 70 | } 71 | } 72 | }; 73 | 74 | script_args.extend(arguments.map(Into::into)); 75 | 76 | Ok( 77 | Command::Run( 78 | Args { 79 | script_path, 80 | check: matches.is_present("check"), 81 | print_lexemes: matches.is_present("lex"), 82 | print_ast: matches.is_present("ast"), 83 | print_program: matches.is_present("program"), 84 | script_args: script_args.into_boxed_slice(), 85 | } 86 | ) 87 | ) 88 | }, 89 | 90 | Err(error) => match error.kind { 91 | clap::ErrorKind::HelpDisplayed => Ok( 92 | Command::Help(error.message.into_boxed_str()) 93 | ), 94 | clap::ErrorKind::VersionDisplayed => Ok( 95 | Command::Version(error.message.into_boxed_str()) 96 | ), 97 | _ => Err(error) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | /// A Display-like trait that takes an additional context when formatting. 4 | /// This is needed to have access to the string interner when formating the AST or error 5 | /// messages. 6 | pub trait Display<'a> { 7 | /// The format context. 8 | type Context: 'a; 9 | 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>, context: Self::Context) -> std::fmt::Result; 11 | } 12 | 13 | 14 | impl<'a, T> Display<'a> for &T 15 | where 16 | T: Display<'a>, 17 | { 18 | type Context = T::Context; 19 | 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>, context: Self::Context) -> std::fmt::Result { 21 | (*self).fmt(f, context) 22 | } 23 | } 24 | 25 | 26 | impl<'a, T> Display<'a> for &mut T 27 | where 28 | T: Display<'a>, 29 | { 30 | type Context = T::Context; 31 | 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>, context: Self::Context) -> std::fmt::Result { 33 | (**self).fmt(f, context) 34 | } 35 | } 36 | 37 | 38 | /// An adapter to use std::fmt::Display with the contextual Display. 39 | #[derive(Debug)] 40 | pub struct Show(pub T, pub C); 41 | 42 | 43 | impl<'a, T, C> std::fmt::Display for Show 44 | where 45 | T: Display<'a, Context = C>, 46 | C: Copy, 47 | { 48 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 49 | self.0.fmt(f, self.1) 50 | } 51 | } 52 | 53 | 54 | /// A ToString-like trait that takes an additional context when formatting. 55 | /// This is needed to have access to the string interner when formating the AST or error 56 | /// messages. 57 | pub trait FmtString<'a> { 58 | /// The format context. 59 | type Context: 'a; 60 | 61 | fn fmt_string(&self, context: Self::Context) -> String; 62 | } 63 | 64 | 65 | impl<'a, T> FmtString<'a> for T 66 | where 67 | T: Display<'a>, 68 | T::Context: Copy, 69 | { 70 | type Context = T::Context; 71 | 72 | fn fmt_string(&self, context: Self::Context) -> String { 73 | let mut string = String::new(); 74 | write!(string, "{}", Show(self, context)) 75 | .expect("a Display implementation returned an error unexpectedly"); 76 | string 77 | } 78 | } 79 | 80 | 81 | /// An indentation level. Each level corresponds to one tabulation character. 82 | #[derive(Debug, Default, Copy, Clone)] 83 | pub struct Indentation(pub u8); 84 | 85 | 86 | impl Indentation { 87 | /// Increase the identation level by one. 88 | pub fn increase(self) -> Self { 89 | Indentation(self.0 + 1) 90 | } 91 | } 92 | 93 | 94 | impl std::fmt::Display for Indentation { 95 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 96 | for _ in 0 .. self.0 { 97 | f.write_char('\t')?; 98 | } 99 | 100 | Ok(()) 101 | } 102 | } 103 | 104 | 105 | /// Format a sequence of items with a separator. 106 | pub fn sep_by( 107 | mut iter: I, 108 | f: &mut std::fmt::Formatter, 109 | mut format: F, 110 | separator: S, 111 | ) -> std::fmt::Result 112 | where 113 | I: Iterator, 114 | F: FnMut(T, &mut std::fmt::Formatter) -> std::fmt::Result, 115 | S: std::fmt::Display, 116 | { 117 | if let Some(item) = iter.next() { 118 | format(item, f)?; 119 | } 120 | 121 | for item in iter { 122 | separator.fmt(f)?; 123 | format(item, f)?; 124 | } 125 | 126 | Ok(()) 127 | } 128 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::prelude::{AsRawFd, RawFd}; 2 | 3 | 4 | pub type FileDescriptor = RawFd; 5 | 6 | 7 | /// Get the file descriptor for stdout. 8 | pub fn stdout_fd() -> FileDescriptor { 9 | std::io::stdout().as_raw_fd() 10 | } 11 | -------------------------------------------------------------------------------- /src/runtime/command/arg.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | ffi::OsString, 4 | os::unix::ffi::OsStringExt, 5 | }; 6 | 7 | use super::exec; 8 | 9 | 10 | pub type Arg = Vec; 11 | 12 | 13 | pub enum Args { 14 | Patterns(Vec), 15 | Literals(Vec), 16 | } 17 | 18 | 19 | impl Args { 20 | pub fn push_literal(&mut self, literal: &[u8]) { 21 | match self { 22 | Self::Patterns(patterns) => { 23 | if patterns.is_empty() { 24 | patterns.push(Arg::default()); 25 | } 26 | 27 | let escaped = Self::pattern_escape(literal); 28 | let escaped = escaped.as_ref(); 29 | 30 | for pattern in patterns.iter_mut() { 31 | pattern.extend(escaped); 32 | } 33 | } 34 | 35 | Self::Literals(literals) => { 36 | if literals.is_empty() { 37 | literals.push(Arg::default()); 38 | } 39 | 40 | for lit in literals.iter_mut() { 41 | lit.extend(literal); 42 | } 43 | } 44 | } 45 | } 46 | 47 | 48 | pub fn push_pattern(&mut self, pattern: &[u8]) { 49 | match self { 50 | Self::Patterns(patterns) => { 51 | if patterns.is_empty() { 52 | patterns.push(Arg::default()); 53 | } 54 | 55 | for rx in patterns.iter_mut() { 56 | rx.extend(pattern); 57 | } 58 | } 59 | 60 | Self::Literals(literals) => { 61 | if literals.is_empty() { 62 | literals.push(Arg::default()); 63 | } 64 | 65 | let mut patterns = std::mem::take(literals); 66 | 67 | for literal in patterns.iter_mut() { 68 | if let Cow::Owned(mut lit) = Self::pattern_escape(literal) { 69 | std::mem::swap(&mut lit, literal); 70 | }; 71 | 72 | literal.extend(pattern); 73 | } 74 | 75 | *self = Self::Patterns(patterns); 76 | } 77 | } 78 | } 79 | 80 | 81 | /// Push many literals in a cartesian product style. 82 | pub fn push_literals(&mut self, mut iter: I) 83 | where 84 | I: Iterator, 85 | B: AsRef<[u8]>, 86 | { 87 | if let Some(first) = iter.next() { 88 | let first = first.as_ref(); 89 | 90 | let (args, escape) = match self { 91 | Args::Patterns(patterns) => (patterns, true), 92 | Args::Literals(literals) => (literals, false), 93 | }; 94 | 95 | if args.is_empty() { 96 | args.push(Arg::default()); 97 | } 98 | 99 | let original_len = args.len(); 100 | 101 | for lit in iter { 102 | let lit = lit.as_ref(); 103 | 104 | let previous_len = args.len(); 105 | args.extend_from_within(..original_len); 106 | 107 | let lit = 108 | if escape { 109 | Self::pattern_escape(lit) 110 | } else { 111 | lit.into() 112 | }; 113 | 114 | for arg in args[previous_len..].iter_mut() { 115 | arg.extend(lit.as_ref()); 116 | } 117 | } 118 | 119 | let first = 120 | if escape { 121 | Self::pattern_escape(first) 122 | } else { 123 | first.into() 124 | }; 125 | 126 | for arg in args[..original_len].iter_mut() { 127 | arg.extend(first.as_ref()); 128 | } 129 | } 130 | } 131 | 132 | 133 | fn pattern_escape(literal: &[u8]) -> Cow<[u8]> { 134 | let has_meta = literal 135 | .iter() 136 | .copied() 137 | .any(Self::is_pattern_meta); 138 | 139 | if has_meta { 140 | let mut escaped = Vec::with_capacity(literal.len()); 141 | 142 | for character in literal.iter().copied() { 143 | if Self::is_pattern_meta(character) { 144 | escaped.push(b'['); 145 | escaped.push(character); 146 | escaped.push(b']'); 147 | } else { 148 | escaped.push(character) 149 | } 150 | } 151 | 152 | Cow::Owned(escaped) 153 | } else { 154 | Cow::Borrowed(literal) 155 | } 156 | } 157 | 158 | 159 | fn is_pattern_meta(c: u8) -> bool { 160 | matches!(c, b'?' | b'*' | b'[' | b']') 161 | } 162 | } 163 | 164 | 165 | impl Default for Args { 166 | fn default() -> Self { 167 | Self::Literals(Vec::new()) 168 | } 169 | } 170 | 171 | 172 | impl From for Box<[exec::Argument]> { 173 | fn from(args: Args) -> Box<[exec::Argument]> { 174 | match args { 175 | Args::Patterns(patterns) => { 176 | patterns 177 | .into_iter() 178 | .map( 179 | |pattern| exec::Argument::Pattern( 180 | OsString::from_vec(pattern).into_boxed_os_str() 181 | ) 182 | ) 183 | .collect() 184 | } 185 | 186 | Args::Literals(literals) => { 187 | literals 188 | .into_iter() 189 | .map( 190 | |lit| exec::Argument::Literal( 191 | OsString::from_vec(lit).into_boxed_os_str() 192 | ) 193 | ) 194 | .collect() 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/runtime/command/exec/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | os::unix::ffi::OsStrExt, 4 | }; 5 | 6 | use super::{Argument, RedirectionTarget, Redirection, Builtin, BasicCommand, Command, Block}; 7 | 8 | use crate::{ 9 | syntax::lexer::CommandOperator, 10 | fmt::{self, Indentation}, 11 | term::color, 12 | }; 13 | 14 | 15 | impl Display for Argument { 16 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 17 | '"'.fmt(f)?; 18 | 19 | match self { 20 | Self::Pattern(pattern) => String::from_utf8_lossy(pattern.as_bytes()).escape_debug().fmt(f)?, 21 | Self::Literal(lit) => String::from_utf8_lossy(lit.as_bytes()).escape_debug().fmt(f)?, 22 | }; 23 | 24 | '"'.fmt(f) 25 | } 26 | } 27 | 28 | 29 | impl Display for RedirectionTarget { 30 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 31 | match self { 32 | Self::Fd(fd) => write!(f, ">{}", fd), 33 | 34 | Self::Overwrite(arg) => { 35 | ">".fmt(f)?; 36 | arg.fmt(f) 37 | } 38 | 39 | Self::Append(arg) => { 40 | ">>".fmt(f)?; 41 | arg.fmt(f) 42 | }, 43 | } 44 | } 45 | } 46 | 47 | 48 | impl Display for Redirection { 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 50 | match self { 51 | Self::Output { source, target } => { 52 | source.fmt(f)?; 53 | target.fmt(f) 54 | } 55 | 56 | Self::Input { literal: false, source } => { 57 | "<".fmt(f)?; 58 | source.fmt(f) 59 | } 60 | 61 | Self::Input { literal: true, source } => { 62 | "<<".fmt(f)?; 63 | source.fmt(f) 64 | } 65 | } 66 | } 67 | } 68 | 69 | 70 | impl Display for Builtin { 71 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 72 | let command = match self { 73 | Self::Alias => "alias", 74 | Self::Cd => "cd", 75 | Self::Exec => "exec", 76 | Self::Exec0 => "exec0", 77 | Self::Spawn0 => "spawn0", 78 | }; 79 | 80 | color::Fg(color::Green, command).fmt(f) 81 | } 82 | } 83 | 84 | 85 | impl Display for BasicCommand { 86 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 87 | self.program.fmt(f)?; 88 | 89 | for arg in self.arguments.iter() { 90 | " ".fmt(f)?; 91 | arg.fmt(f)?; 92 | } 93 | 94 | for redirection in self.redirections.iter() { 95 | " ".fmt(f)?; 96 | redirection.fmt(f)?; 97 | } 98 | 99 | if self.abort_on_error { 100 | " ".fmt(f)?; 101 | CommandOperator::Try.fmt(f)?; 102 | } 103 | 104 | Ok(()) 105 | } 106 | } 107 | 108 | 109 | impl Display for Command { 110 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 111 | match self { 112 | Self::Builtin { program, arguments, abort_on_error, .. } => { 113 | program.fmt(f)?; 114 | 115 | for arg in arguments.iter() { 116 | " ".fmt(f)?; 117 | arg.fmt(f)?; 118 | } 119 | 120 | if *abort_on_error { 121 | " ".fmt(f)?; 122 | CommandOperator::Try.fmt(f)?; 123 | } 124 | } 125 | 126 | Self::External { head, tail } => { 127 | head.fmt(f)?; 128 | 129 | for command in tail.iter() { 130 | "\n".fmt(f)?; 131 | Indentation(2).fmt(f)?; 132 | color::Fg(color::Yellow, "|").fmt(f)?; 133 | " ".fmt(f)?; 134 | command.fmt(f)?; 135 | } 136 | } 137 | } 138 | 139 | Ok(()) 140 | } 141 | } 142 | 143 | 144 | impl Display for Block { 145 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 146 | "{\n".fmt(f)?; 147 | 148 | fmt::sep_by( 149 | std::iter::once(&self.head).chain(self.tail.iter()), 150 | f, 151 | |cmd, f| { 152 | Indentation(1).fmt(f)?; 153 | cmd.fmt(f) 154 | }, 155 | ";\n", 156 | )?; 157 | 158 | "\n}".fmt(f) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/runtime/command/exec/join.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, GcCell, Trace}; 2 | 3 | use crate::runtime::value::{CallContext, NativeFun, Value}; 4 | 5 | use super::{Panic, PipelineErrors, IntoValue}; 6 | 7 | 8 | #[derive(Finalize)] 9 | struct JoinHandle( 10 | std::thread::JoinHandle, Panic>> 11 | ); 12 | 13 | 14 | unsafe impl Trace for JoinHandle { 15 | gc::unsafe_empty_trace!(); 16 | } 17 | 18 | 19 | #[derive(Trace, Finalize)] 20 | pub struct Join(GcCell>); 21 | 22 | 23 | impl Join { 24 | pub fn new(handle: std::thread::JoinHandle, Panic>>) -> Self { 25 | Self( 26 | GcCell::new( 27 | Some(JoinHandle(handle)) 28 | ) 29 | ) 30 | } 31 | } 32 | 33 | 34 | impl NativeFun for Join { 35 | fn name(&self) -> &'static str { ".join" } 36 | 37 | fn call(&self, context: CallContext) -> Result { 38 | match self.0.borrow_mut().take() { 39 | Some(JoinHandle(join_handle)) => { 40 | let result = match join_handle.join() { 41 | Ok(result) => result, 42 | Err(error) => std::panic::resume_unwind(error), 43 | }; 44 | 45 | result 46 | .map(|errors| errors.into_value(context.interner())) 47 | .map_err(Into::into) 48 | }, 49 | 50 | None => Err( 51 | crate::runtime::Panic::invalid_join(context.pos), 52 | ) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/runtime/flow.rs: -------------------------------------------------------------------------------- 1 | use super::value::Value; 2 | 3 | 4 | /// Control flow in the language. 5 | #[derive(Debug)] 6 | pub enum Flow { 7 | /// Regular flow: follow everything in order. 8 | Regular(Value), 9 | /// Return from function. 10 | Return(Value), 11 | /// Break from loop. 12 | Break, 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime/lib.rs: -------------------------------------------------------------------------------- 1 | automod::dir!("src/runtime/lib"); 2 | 3 | use super::{ 4 | keys, 5 | Array, 6 | CallContext, 7 | Dict, 8 | Error, 9 | Float, 10 | Function, 11 | NativeFun, 12 | RustFun, 13 | Panic, 14 | Str, 15 | Value, 16 | Type, 17 | }; 18 | 19 | 20 | inventory::collect!(RustFun); 21 | 22 | 23 | /// Instantiate the stdlib. 24 | pub fn new() -> Value { 25 | let mut dict = Dict::default(); 26 | 27 | for fun in inventory::iter:: { 28 | let path = fun 29 | .name() 30 | .strip_prefix("std.") 31 | .expect("Builtin function name missing std prefix."); 32 | 33 | insert(path, fun.copy().into(), &mut dict); 34 | } 35 | 36 | dict.into() 37 | } 38 | 39 | 40 | fn insert(path: &str, value: Value, dict: &mut Dict) { 41 | match path.split_once('.') { 42 | None => dict.insert(path.into(), value), 43 | Some((key, path)) => { 44 | let mut dict = dict.borrow_mut(); 45 | let dict = dict.entry(key.into()).or_insert_with(|| Dict::default().into()); 46 | 47 | match dict { 48 | Value::Dict(dict) => insert(path, value, dict), 49 | _ => panic!("invalid value in std initialization"), 50 | } 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/runtime/lib/args.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Args) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Args; 16 | 17 | impl NativeFun for Args { 18 | fn name(&self) -> &'static str { "std.args" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [] => Ok(context.runtime.args.copy()), 23 | args => Err(Panic::invalid_args(args.len() as u32, 0, context.pos)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime/lib/assert.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | NativeFun, 6 | RustFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit!{ RustFun::from(Assert) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Assert; 16 | 17 | impl NativeFun for Assert { 18 | fn name(&self) -> &'static str { "std.assert" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Bool(true) ] => Ok(Value::default()), 23 | [ Value::Bool(false) ] => Err(Panic::assertion_failed(context.pos)), 24 | 25 | [ other ] => Err(Panic::type_error(other.copy(), "bool", context.pos)), 26 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/runtime/lib/base64.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | Error, 5 | NativeFun, 6 | Panic, 7 | RustFun, 8 | Value, 9 | Str, 10 | CallContext, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Encode) } 15 | inventory::submit! { RustFun::from(Decode) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Encode; 19 | 20 | impl NativeFun for Encode { 21 | fn name(&self) -> &'static str { "std.base64.encode" } 22 | 23 | fn call(&self, context: CallContext) -> Result { 24 | match context.args() { 25 | [ Value::String(ref string) ] => Ok(base64::encode(string).into()), 26 | 27 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 28 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 29 | } 30 | } 31 | } 32 | 33 | #[derive(Trace, Finalize)] 34 | struct Decode; 35 | 36 | impl NativeFun for Decode { 37 | fn name(&self) -> &'static str { "std.base64.decode" } 38 | 39 | fn call(&self, context: CallContext) -> Result { 40 | match context.args() { 41 | [ value @ Value::String(ref string) ] => Ok( 42 | base64::decode(string) 43 | .map(|value| Str::from(value).into()) 44 | .unwrap_or_else( 45 | |error| Error::new(error.to_string().into(), value.copy()).into() 46 | ) 47 | ), 48 | 49 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 50 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/runtime/lib/bind.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | Function, 6 | NativeFun, 7 | RustFun, 8 | Panic, 9 | Value, 10 | }; 11 | 12 | 13 | inventory::submit!{ RustFun::from(Bind) } 14 | 15 | #[derive(Trace, Finalize)] 16 | struct Bind; 17 | 18 | impl NativeFun for Bind { 19 | fn name(&self) -> &'static str { "std.bind" } 20 | 21 | fn call(&self, context: CallContext) -> Result { 22 | match context.args() { 23 | [ obj, Value::Function(fun) ] => Ok( 24 | BindImpl { 25 | obj: obj.copy(), 26 | function: fun.copy(), 27 | }.into() 28 | ), 29 | 30 | [ _, other ] => Err(Panic::type_error(other.copy(), "function", context.pos)), 31 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 32 | } 33 | } 34 | } 35 | 36 | 37 | #[derive(Trace, Finalize)] 38 | struct BindImpl { 39 | obj: Value, 40 | function: Function, 41 | } 42 | 43 | impl NativeFun for BindImpl { 44 | fn name(&self) -> &'static str { "std.bind" } 45 | 46 | fn call(&self, mut context: CallContext) -> Result { 47 | context.call(self.obj.copy(), &self.function, context.args_start) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/runtime/lib/bytes.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Bytes) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Bytes; 16 | 17 | impl NativeFun for Bytes { 18 | fn name(&self) -> &'static str { "std.bytes" } 19 | 20 | fn call(&self, mut context: CallContext) -> Result { 21 | match context.args_mut() { 22 | [ Value::String(ref string) ] => Ok( 23 | string 24 | .as_bytes() 25 | .iter() 26 | .copied() 27 | .map(Value::Byte) 28 | .collect::>() 29 | .into() 30 | ), 31 | 32 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 33 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/runtime/lib/catch.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use crate::fmt; 4 | 5 | use super::{ 6 | CallContext, 7 | Error, 8 | NativeFun, 9 | RustFun, 10 | Panic, 11 | Value, 12 | }; 13 | 14 | 15 | inventory::submit!{ RustFun::from(Catch) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Catch; 19 | 20 | impl NativeFun for Catch { 21 | fn name(&self) -> &'static str { "std.catch" } 22 | 23 | fn call(&self, mut context: CallContext) -> Result { 24 | thread_local! { 25 | pub static PANIC: Value = "panic".into(); 26 | } 27 | 28 | let fun = match context.args() { 29 | [ Value::Function(fun) ] => fun.copy(), 30 | 31 | [ other ] => return Err(Panic::type_error(other.copy(), "function", context.pos)), 32 | args => return Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 33 | }; 34 | 35 | let result = context.call( 36 | Value::default(), 37 | &fun, 38 | context.args_start + 1 39 | ); 40 | 41 | match result { 42 | Ok(value) => Ok(value), 43 | 44 | Err(panic) => { 45 | let description = format!( 46 | "caught panic: {}", 47 | fmt::Show(panic, context.interner()), 48 | ); 49 | 50 | Ok( 51 | Value::from( 52 | Error::new( 53 | description.into(), 54 | PANIC.with(Value::copy), 55 | ) 56 | ) 57 | ) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/runtime/lib/cd.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | NativeFun, 8 | RustFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit!{ RustFun::from(Cd) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Cd; 18 | 19 | impl NativeFun for Cd { 20 | fn name(&self) -> &'static str { "std.cd" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | match context.args() { 24 | [ Value::String(ref string) ] => Ok( 25 | std::env 26 | ::set_current_dir(AsRef::::as_ref(string)) 27 | .into() 28 | ), 29 | 30 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 31 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/runtime/lib/contains.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Contains) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Contains; 16 | 17 | impl NativeFun for Contains { 18 | fn name(&self) -> &'static str { "std.contains" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Array(ref array), item ] => Ok(array.contains(item).into()), 23 | 24 | [ Value::Dict(ref dict), key ] => Ok(dict.contains(key).into()), 25 | 26 | [ Value::String(ref string), Value::Byte(byte) ] => Ok(string.contains(*byte).into()), 27 | [ Value::String(_), other ] => Err(Panic::type_error(other.copy(), "char", context.pos)), 28 | 29 | [ other, _ ] => Err(Panic::type_error(other.copy(), "string ,array or dict", context.pos)), 30 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/runtime/lib/cwd.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Cwd) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Cwd; 18 | 19 | impl NativeFun for Cwd { 20 | fn name(&self) -> &'static str { "std.cwd" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | let args = context.args(); 24 | if !args.is_empty() { 25 | return Err(Panic::invalid_args(args.len() as u32, 0, context.pos)); 26 | } 27 | 28 | Ok( 29 | std::env 30 | ::current_dir() 31 | .map(PathBuf::into_os_string) 32 | .into() 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/runtime/lib/env.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use crate::runtime::value::Error; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Env) } 15 | inventory::submit! { RustFun::from(Export) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Env; 19 | 20 | impl NativeFun for Env { 21 | fn name(&self) -> &'static str { "std.env" } 22 | 23 | fn call(&self, context: CallContext) -> Result { 24 | match context.args() { 25 | [ Value::String(ref string) ] => Ok( 26 | std::env 27 | ::var_os(string) 28 | .map(Value::from) 29 | .unwrap_or(Value::Nil) 30 | ), 31 | 32 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 33 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 34 | } 35 | } 36 | } 37 | 38 | #[derive(Trace, Finalize)] 39 | struct Export; 40 | 41 | impl NativeFun for Export { 42 | fn name(&self) -> &'static str { "std.export" } 43 | 44 | fn call(&self, context: CallContext) -> Result { 45 | match context.args() { 46 | [ k @ Value::String(ref key), v @ Value::String(ref value) ] => { 47 | let ret = match () { 48 | () if key.contains(b'=') || key.contains(b'\0') => { 49 | Error::new("invalid export key".into(), k.copy()).into() 50 | } 51 | 52 | () if value.contains(b'\0') => { 53 | Error::new("invalid export value".into(), v.copy()).into() 54 | } 55 | 56 | () => { 57 | std::env::set_var(key, value); 58 | Value::default() 59 | }, 60 | }; 61 | 62 | Ok(ret) 63 | }, 64 | 65 | [ Value::String(_), other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 66 | [ other, _ ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 67 | 68 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/runtime/lib/error.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | Error, 6 | RustFun, 7 | NativeFun, 8 | Panic, 9 | Value, 10 | }; 11 | 12 | 13 | inventory::submit! { RustFun::from(ErrorFun) } 14 | 15 | #[derive(Trace, Finalize)] 16 | struct ErrorFun; 17 | 18 | impl NativeFun for ErrorFun { 19 | fn name(&self) -> &'static str { "std.error" } 20 | 21 | fn call(&self, context: CallContext) -> Result { 22 | match context.args() { 23 | [ Value::String(ref string), context ] => Ok( 24 | Error 25 | ::new(string.copy(), context.copy()) 26 | .into() 27 | ), 28 | 29 | [ other, _ ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 30 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/runtime/lib/exit.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | NativeFun, 8 | RustFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit!{ RustFun::from(Exit) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Exit; 18 | 19 | impl NativeFun for Exit { 20 | fn name(&self) -> &'static str { "std.exit" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | match context.args() { 24 | [ val @ Value::Int(i) ] => { 25 | let code = u8::try_from(*i) 26 | .map_err(|_| Panic::value_error(val.copy(), "valid exit code", context.pos.copy()))?; 27 | 28 | std::process::exit(code.into()) 29 | } 30 | 31 | [ other ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 32 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/runtime/lib/float.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | NativeFun, 6 | RustFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit!{ RustFun::from(Float) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Float; 16 | 17 | impl NativeFun for Float { 18 | fn name(&self) -> &'static str { "std.float" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Float(f) ] => Ok( 23 | Value::Float(f.copy()) 24 | ), 25 | 26 | [ Value::Int(i) ] => Ok( 27 | Value::Float(i.into()) 28 | ), 29 | 30 | [ value @ Value::String(ref string) ] => { 31 | let parse_error = || Panic::value_error( 32 | value.copy(), 33 | "valid integer", 34 | context.pos.copy() 35 | ); 36 | 37 | let slice = std::str 38 | ::from_utf8(string.as_bytes()) 39 | .map_err(|_| parse_error())?; 40 | 41 | let float: f64 = slice 42 | .parse() 43 | .map_err(|_| parse_error())?; 44 | 45 | Ok(Value::from(float)) 46 | } 47 | 48 | [ other ] => Err(Panic::type_error(other.copy(), "int, float or string", context.pos)), 49 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/runtime/lib/glob.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | Error, 10 | }; 11 | 12 | 13 | inventory::submit! { RustFun::from(Glob) } 14 | 15 | #[derive(Trace, Finalize)] 16 | struct Glob; 17 | 18 | impl Glob { 19 | fn glob(pattern: &[u8]) -> Result { 20 | let pattern = std::str::from_utf8(pattern).map_err(|_| Error::new("Invalid pattern".into(), Value::default()))?; 21 | let paths = glob::glob(pattern).map_err(|error| Error::new("Invalid pattern".into(), error.msg.into()))?; 22 | let paths: Vec = paths 23 | .map(|result| result 24 | .map(|path| Value::String(path.into())) 25 | .map_err(|error| error.into_error().into()) 26 | ) 27 | .collect::>()?; 28 | Ok(paths.into()) 29 | } 30 | } 31 | 32 | impl NativeFun for Glob { 33 | fn name(&self) -> &'static str { "std.glob" } 34 | 35 | fn call(&self, context: CallContext) -> Result { 36 | match context.args() { 37 | [ Value::String(ref string) ] => { 38 | let result = Self::glob(string.as_ref()); 39 | Ok(result.unwrap_or_else(Into::into)) 40 | }, 41 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 42 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/runtime/lib/has_error.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(HasError) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct HasError; 16 | 17 | impl HasError { 18 | fn has_error(value: &Value) -> bool { 19 | match value { 20 | Value::Error(_) => true, 21 | 22 | Value::Array(array) => { 23 | for value in array.borrow().iter() { 24 | if Self::has_error(value) { 25 | return true; 26 | } 27 | } 28 | 29 | false 30 | } 31 | 32 | Value::Dict(dict) => { 33 | for (key, value) in dict.borrow().iter() { 34 | if Self::has_error(key) || Self::has_error(value) { 35 | return true; 36 | } 37 | } 38 | 39 | false 40 | } 41 | 42 | _ => false, 43 | } 44 | } 45 | } 46 | 47 | 48 | impl NativeFun for HasError { 49 | fn name(&self) -> &'static str { "std.has_error" } 50 | 51 | fn call(&self, context: CallContext) -> Result { 52 | match context.args() { 53 | [ value ] => Ok(Self::has_error(value).into()), 54 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/runtime/lib/hex.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | Error, 5 | NativeFun, 6 | Panic, 7 | RustFun, 8 | Value, 9 | Str, 10 | CallContext, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Encode) } 15 | inventory::submit! { RustFun::from(Decode) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Encode; 19 | 20 | impl NativeFun for Encode { 21 | fn name(&self) -> &'static str { "std.hex.encode" } 22 | 23 | fn call(&self, context: CallContext) -> Result { 24 | match context.args() { 25 | [ Value::String(ref string) ] => Ok(hex::encode(string).into()), 26 | 27 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 28 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 29 | } 30 | } 31 | } 32 | 33 | #[derive(Trace, Finalize)] 34 | struct Decode; 35 | 36 | impl NativeFun for Decode { 37 | fn name(&self) -> &'static str { "std.hex.decode" } 38 | 39 | fn call(&self, context: CallContext) -> Result { 40 | match context.args() { 41 | [ value @ Value::String(ref string) ] => Ok( 42 | hex::decode(string) 43 | .map(|value| Str::from(value).into()) 44 | .unwrap_or_else( 45 | |error| Error::new(error.to_string().into(), value.copy()).into() 46 | ) 47 | ), 48 | 49 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 50 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/runtime/lib/import.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | path::{Path, PathBuf}, 4 | ffi::OsStr, 5 | os::unix::ffi::OsStrExt, 6 | }; 7 | 8 | use gc::{Finalize, Trace}; 9 | 10 | use crate::{ 11 | fmt, 12 | syntax, 13 | semantic, 14 | symbol::{self, Symbol} 15 | }; 16 | use super::{ 17 | CallContext, 18 | RustFun, 19 | NativeFun, 20 | Panic, 21 | Value, 22 | }; 23 | 24 | 25 | inventory::submit! { RustFun::from(Import) } 26 | 27 | #[derive(Trace, Finalize)] 28 | struct Import; 29 | 30 | impl Import { 31 | fn import(module_path: &Path, mut context: CallContext) -> Result { 32 | let path = Self 33 | ::resolve_path( 34 | module_path, 35 | context.pos.path, 36 | context.runtime.interner_mut() 37 | ) 38 | .map_err( 39 | |error| Panic::io(error, context.pos.copy()) 40 | )?; 41 | 42 | match context.runtime.modules.get(&path) { 43 | Some(module) => Ok(module.copy()), // Don't reload module if cached. 44 | None => { 45 | let module = Self::load(path, &mut context)?; 46 | context.runtime.modules.insert(path, module.copy()); 47 | Ok(module) 48 | } 49 | } 50 | } 51 | 52 | 53 | fn resolve_path( 54 | target_path: &Path, 55 | current_path: Symbol, 56 | interner: &mut symbol::Interner, 57 | ) -> io::Result { 58 | let mut path_buf = PathBuf::from( 59 | OsStr::from_bytes( 60 | interner 61 | .resolve(current_path) 62 | .expect("failed to resolve symbol") 63 | ).to_owned() 64 | ); 65 | path_buf.pop(); // Remove the file name. 66 | path_buf.push(target_path); 67 | 68 | let path = path_buf.canonicalize()?; 69 | 70 | let path_symbol = interner.get_or_intern( 71 | path 72 | .as_os_str() 73 | .as_bytes() 74 | ); 75 | 76 | Ok(path_symbol) 77 | } 78 | 79 | 80 | fn load(path: Symbol, context: &mut CallContext) -> Result { 81 | // Load file. 82 | let source = syntax::Source 83 | ::from_path( 84 | path, 85 | context.runtime.interner_mut() 86 | ).map_err( 87 | |error| Panic::io(error, context.pos.copy()) 88 | )?; 89 | 90 | // Syntax. 91 | let syntactic_analysis = syntax::Analysis::analyze( 92 | &source, 93 | context.runtime.interner_mut() 94 | ); 95 | let has_syntax_errors = !syntactic_analysis.is_ok(); 96 | 97 | if has_syntax_errors { 98 | eprint!("{}", fmt::Show( 99 | syntactic_analysis.errors, 100 | syntax::AnalysisDisplayContext { 101 | max_errors: Some(20), 102 | interner: context.runtime.interner(), 103 | } 104 | )); 105 | return Err(Panic::import_failed(path, context.pos.copy())); 106 | } 107 | 108 | // Semantics. 109 | let program = semantic::Analyzer 110 | ::analyze( 111 | syntactic_analysis.ast, context.runtime.interner_mut() 112 | ) 113 | .map_err( 114 | |errors| { 115 | eprint!("{}", fmt::Show( 116 | errors, 117 | semantic::ErrorsDisplayContext { 118 | max_errors: Some(20), 119 | interner: context.runtime.interner(), 120 | } 121 | )); 122 | 123 | Panic::import_failed(path, context.pos.copy()) 124 | } 125 | )?; 126 | 127 | // Eval. 128 | let program = Box::leak(Box::new(program)); 129 | context.runtime.eval(program) 130 | } 131 | } 132 | 133 | impl NativeFun for Import { 134 | fn name(&self) -> &'static str { "std.import" } 135 | 136 | fn call(&self, context: CallContext) -> Result { 137 | let path = match context.args() { 138 | [ Value::String(ref string) ] => Path::new(string).to_owned(), 139 | 140 | [ other ] => return Err(Panic::type_error(other.copy(), "string", context.pos)), 141 | 142 | args => return Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 143 | }; 144 | 145 | Self::import(&path, context) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/runtime/lib/int.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | NativeFun, 6 | RustFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit!{ RustFun::from(Int) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Int; 16 | 17 | impl NativeFun for Int { 18 | fn name(&self) -> &'static str { "std.int" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Int(i) ] => Ok( 23 | Value::Int(*i) 24 | ), 25 | 26 | [ Value::Float(f) ] => Ok( 27 | Value::Int(f.into()) 28 | ), 29 | 30 | [ value @ Value::String(ref string) ] => { 31 | let parse_error = || Panic::value_error( 32 | value.copy(), 33 | "valid integer", 34 | context.pos.copy() 35 | ); 36 | 37 | let slice = std::str 38 | ::from_utf8(string.as_bytes()) 39 | .map_err(|_| parse_error())?; 40 | 41 | let int: i64 = slice 42 | .parse() 43 | .map_err(|_| parse_error())?; 44 | 45 | Ok(Value::from(int)) 46 | } 47 | 48 | [ other ] => Err(Panic::type_error(other.copy(), "int, float or string", context.pos)), 49 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/runtime/lib/is_empty.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(IsEmpty) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct IsEmpty; 16 | 17 | impl NativeFun for IsEmpty { 18 | fn name(&self) -> &'static str { "std.is_empty" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Array(ref array) ] => Ok(array.is_empty().into()), 23 | 24 | [ Value::Dict(ref dict) ] => Ok(dict.is_empty().into()), 25 | 26 | [ Value::String(ref string) ] => Ok(string.is_empty().into()), 27 | 28 | [ other ] => Err(Panic::type_error(other.copy(), "string, array or dict", context.pos)), 29 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/runtime/lib/iter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use gc::{Finalize, GcCell, Trace}; 4 | 5 | use super::{ 6 | keys, 7 | Array, 8 | CallContext, 9 | Dict, 10 | RustFun, 11 | NativeFun, 12 | Panic, 13 | Str, 14 | Value, 15 | }; 16 | 17 | 18 | inventory::submit! { RustFun::from(Iter) } 19 | 20 | #[derive(Trace, Finalize)] 21 | struct Iter; 22 | 23 | impl NativeFun for Iter { 24 | fn name(&self) -> &'static str { "std.iter" } 25 | 26 | fn call(&self, context: CallContext) -> Result { 27 | match context.args() { 28 | [ Value::Array(ref array) ] => Ok( 29 | IterImpl::Array { 30 | array: array.copy(), 31 | ix: GcCell::new(0), 32 | }.into() 33 | ), 34 | 35 | [ Value::Dict(ref dict) ] => Ok( 36 | IterImpl::Dict { 37 | entries: GcCell::new( 38 | dict 39 | .borrow() 40 | .iter() 41 | .map(|(k, v)| (k.copy(), v.copy())) 42 | .collect() 43 | ) 44 | }.into() 45 | ), 46 | 47 | [ Value::String(ref string) ] => Ok( 48 | IterImpl::String { 49 | string: string.copy(), 50 | ix: GcCell::new(0), 51 | }.into() 52 | ), 53 | 54 | [ other ] => Err(Panic::type_error(other.copy(), "string, array or dict", context.pos)), 55 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 56 | } 57 | } 58 | } 59 | 60 | 61 | #[derive(Trace, Finalize)] 62 | enum IterImpl { 63 | Array { 64 | array: Array, 65 | ix: GcCell, 66 | }, 67 | String { 68 | string: Str, 69 | ix: GcCell, 70 | }, 71 | Dict { 72 | entries: GcCell>, 73 | } 74 | } 75 | 76 | impl NativeFun for IterImpl { 77 | fn name(&self) -> &'static str { "std.iter" } 78 | 79 | fn call(&self, context: CallContext) -> Result { 80 | let args = context.args(); 81 | if !args.is_empty() { 82 | return Err(Panic::invalid_args(args.len() as u32, 0, context.pos)); 83 | } 84 | 85 | let mut iteration = HashMap::new(); 86 | 87 | let next = match self { 88 | IterImpl::Array { array, ix } => { 89 | let mut ix = ix.borrow_mut(); 90 | if let Ok(value) = array.index(*ix) { 91 | *ix += 1; 92 | Some(value) 93 | } else { 94 | None 95 | } 96 | } 97 | 98 | IterImpl::String { string, ix } => { 99 | let mut ix = ix.borrow_mut(); 100 | if let Ok(value) = string.index(*ix) { 101 | *ix += 1; 102 | Some(value) 103 | } else { 104 | None 105 | } 106 | } 107 | 108 | IterImpl::Dict { entries } => entries 109 | .borrow_mut() 110 | .pop() 111 | .map( 112 | |(k, v)| { 113 | let mut entry = HashMap::new(); 114 | 115 | keys::KEY.with( 116 | |key| entry.insert(key.copy(), k) 117 | ); 118 | 119 | keys::VALUE.with( 120 | |value| entry.insert(value.copy(), v) 121 | ); 122 | 123 | Dict::new(entry).into() 124 | } 125 | ) 126 | }; 127 | 128 | keys::FINISHED.with( 129 | |finished| iteration.insert(finished.copy(), next.is_none().into()) 130 | ); 131 | 132 | if let Some(next) = next { 133 | keys::VALUE.with( 134 | |value| iteration.insert(value.copy(), next) 135 | ); 136 | } 137 | 138 | Ok(Dict::new(iteration).into()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/runtime/lib/length.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Length) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Length; 16 | 17 | impl NativeFun for Length { 18 | fn name(&self) -> &'static str { "std.len" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ Value::Array(ref array) ] => Ok(Value::Int(array.len())), 23 | [ Value::Dict(ref dict) ] => Ok(Value::Int(dict.len())), 24 | [ Value::String(ref string) ] => Ok(Value::Int(string.len() as i64)), 25 | [ other ] => Err(Panic::type_error(other.copy(), "string, array or dict", context.pos)), 26 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/runtime/lib/panic.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(UserPanic) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct UserPanic; 16 | 17 | impl NativeFun for UserPanic { 18 | fn name(&self) -> &'static str { "std.panic" } 19 | 20 | fn call(&self, context: CallContext) -> Result { 21 | match context.args() { 22 | [ value ] => Err(Panic::user(value.copy(), context.pos)), 23 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime/lib/pop.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Pop) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Pop; 16 | 17 | impl NativeFun for Pop { 18 | fn name(&self) -> &'static str { "std.pop" } 19 | 20 | fn call(&self, mut context: CallContext) -> Result { 21 | match context.args_mut() { 22 | [ Value::Array(ref mut array) ] => { 23 | let value = array 24 | .pop() 25 | .map_err(|_| Panic::empty_collection(context.pos))?; 26 | 27 | Ok(value) 28 | }, 29 | 30 | [ other ] => Err(Panic::type_error(other.copy(), "array", context.pos)), 31 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/runtime/lib/print.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use crate::{fmt, symbol}; 6 | use super::{ 7 | CallContext, 8 | RustFun, 9 | NativeFun, 10 | Panic, 11 | Value, 12 | }; 13 | 14 | 15 | inventory::submit! { RustFun::from(Print) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Print; 19 | 20 | 21 | impl Print { 22 | fn print(value: &Value, interner: &symbol::Interner, mut writer: W) -> io::Result<()> { 23 | match value { 24 | Value::String(string) => writer.write_all(string.as_ref()), 25 | Value::Byte(byte) => writer.write_all(&[*byte]), 26 | value => write!(writer, "{}", fmt::Show(value, interner)), 27 | } 28 | } 29 | } 30 | 31 | 32 | impl NativeFun for Print { 33 | fn name(&self) -> &'static str { "std.print" } 34 | 35 | fn call(&self, context: CallContext) -> Result { 36 | let stdout = io::stdout(); 37 | let mut stdout = stdout.lock(); 38 | 39 | let mut iter = context.args().iter(); 40 | 41 | if let Some(value) = iter.next() { 42 | Self::print(value, context.interner(), &mut stdout) 43 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 44 | } 45 | 46 | for value in iter { 47 | write!(stdout, "\t") 48 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 49 | 50 | Self::print(value, context.interner(), &mut stdout) 51 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 52 | } 53 | 54 | //writeln!(stdout) 55 | //.map_err(|error| Panic::io(error, context.pos))?; 56 | 57 | Ok(Value::default()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/runtime/lib/println.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use crate::{fmt, symbol}; 6 | use super::{ 7 | CallContext, 8 | RustFun, 9 | NativeFun, 10 | Panic, 11 | Value, 12 | }; 13 | 14 | 15 | inventory::submit! { RustFun::from(Println) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Println; 19 | 20 | 21 | impl Println { 22 | fn println(value: &Value, interner: &symbol::Interner, mut writer: W) -> io::Result<()> { 23 | match value { 24 | Value::String(string) => writer.write_all(string.as_ref()), 25 | Value::Byte(byte) => writer.write_all(&[*byte]), 26 | value => write!(writer, "{}", fmt::Show(value, interner)), 27 | } 28 | } 29 | } 30 | 31 | 32 | impl NativeFun for Println { 33 | fn name(&self) -> &'static str { "std.println" } 34 | 35 | fn call(&self, context: CallContext) -> Result { 36 | let stdout = io::stdout(); 37 | let mut stdout = stdout.lock(); 38 | 39 | let mut iter = context.args().iter(); 40 | 41 | if let Some(value) = iter.next() { 42 | Self::println(value, context.interner(), &mut stdout) 43 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 44 | } 45 | 46 | for value in iter { 47 | write!(stdout, "\t") 48 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 49 | 50 | Self::println(value, context.interner(), &mut stdout) 51 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 52 | } 53 | 54 | writeln!(stdout) 55 | .map_err(|error| Panic::io(error, context.pos))?; 56 | 57 | Ok(Value::default()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/runtime/lib/push.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Push) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Push; 16 | 17 | impl NativeFun for Push { 18 | fn name(&self) -> &'static str { "std.push" } 19 | 20 | fn call(&self, mut context: CallContext) -> Result { 21 | match context.args_mut() { 22 | [ Value::Array(ref mut array), value ] => { 23 | array.push(value.copy()); 24 | Ok(Value::Nil) 25 | }, 26 | 27 | [ other, _ ] => Err(Panic::type_error(other.copy(), "array", context.pos)), 28 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime/lib/rand.rs: -------------------------------------------------------------------------------- 1 | //! This module uses the `ChaCha8Rng` pseudo-random generator from the rand 2 | //! crate. It is suitable for tasks such as simulation, but should not be used 3 | //! for applications like criptography or gambling games. 4 | //! see: 5 | 6 | use std::cell::RefCell; 7 | 8 | use gc::{Finalize, Trace}; 9 | use rand::{Rng, SeedableRng, thread_rng}; 10 | use rand_chacha::ChaCha8Rng; 11 | 12 | use super::{ 13 | Float, 14 | NativeFun, 15 | Panic, 16 | RustFun, 17 | Value, 18 | CallContext, 19 | }; 20 | 21 | inventory::submit! { RustFun::from(Rand) } 22 | inventory::submit! { RustFun::from(RandInt) } 23 | inventory::submit! { RustFun::from(RandSeed) } 24 | 25 | thread_local!(static RNG: RefCell = RefCell::new(ChaCha8Rng::from_rng(thread_rng()).unwrap())); 26 | 27 | #[derive(Trace, Finalize)] 28 | struct Rand; 29 | 30 | #[derive(Trace, Finalize)] 31 | struct RandInt; 32 | 33 | #[derive(Trace, Finalize)] 34 | struct RandSeed; 35 | 36 | impl NativeFun for Rand { 37 | fn name(&self) -> &'static str { "std.rand" } 38 | 39 | fn call(&self, context: CallContext) -> Result { 40 | let args = context.args(); 41 | if args.is_empty() { 42 | Ok(Value::Float(Float(RNG.with(|rng| rng.borrow_mut().gen::())))) 43 | } else { 44 | Err(Panic::invalid_args(args.len() as u32, 0, context.pos)) 45 | } 46 | } 47 | } 48 | 49 | impl NativeFun for RandInt { 50 | fn name(&self) -> &'static str { "std.randint" } 51 | 52 | fn call(&self, context: CallContext) -> Result { 53 | match context.args() { 54 | [ Value::Int(m), Value::Int(n) ] => Ok(Value::Int( 55 | RNG.with(|rng| rng.borrow_mut().gen_range(*m..=*n)) 56 | )), 57 | [ other, _ ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 58 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 59 | } 60 | } 61 | } 62 | 63 | impl NativeFun for RandSeed { 64 | fn name(&self) -> &'static str { "std.randseed" } 65 | 66 | fn call(&self, context: CallContext) -> Result { 67 | match context.args() { 68 | [ Value::Int(n) ] => { 69 | RNG.with(|rng| *rng.borrow_mut() = ChaCha8Rng::seed_from_u64(*n as u64)); 70 | Ok(Value::default()) 71 | }, 72 | [ other ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 73 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/runtime/lib/range.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use gc::{Finalize, GcCell, Trace}; 4 | 5 | use super::{ 6 | util, 7 | keys, 8 | CallContext, 9 | Dict, 10 | RustFun, 11 | NativeFun, 12 | Panic, 13 | Value, 14 | }; 15 | 16 | 17 | inventory::submit! { RustFun::from(Range) } 18 | 19 | #[derive(Trace, Finalize)] 20 | struct Range; 21 | 22 | impl NativeFun for Range { 23 | fn name(&self) -> &'static str { "std.range" } 24 | 25 | fn call(&self, context: CallContext) -> Result { 26 | match context.args() { 27 | [ from, to, step ] => { 28 | let numbers = util::Numbers 29 | ::promote([from.copy(), to.copy(), step.copy()]) 30 | .map_err(|value| Panic::type_error(value, "int or float", context.pos))?; 31 | 32 | Ok( 33 | match numbers { 34 | util::Numbers::Ints([ from, to, step ]) => RangeImpl { 35 | from: GcCell::new(from), 36 | to, 37 | step 38 | }.into(), 39 | 40 | util::Numbers::Floats([ from, to, step ]) => RangeImpl { 41 | from: GcCell::new(from), 42 | to, 43 | step 44 | }.into(), 45 | } 46 | ) 47 | }, 48 | 49 | args => Err(Panic::invalid_args(args.len() as u32, 3, context.pos)) 50 | } 51 | } 52 | } 53 | 54 | 55 | #[derive(Trace, Finalize)] 56 | struct RangeImpl { 57 | from: GcCell, 58 | to: T, 59 | step: T, 60 | } 61 | 62 | impl NativeFun for RangeImpl 63 | where 64 | T: Trace + Finalize + 'static, 65 | T: Clone + Default + Ord + std::ops::Add, 66 | T: Into, 67 | { 68 | fn name(&self) -> &'static str { "std.range" } 69 | 70 | fn call(&self, context: CallContext) -> Result { 71 | let args = context.args(); 72 | if !args.is_empty() { 73 | return Err(Panic::invalid_args(args.len() as u32, 0, context.pos)); 74 | } 75 | 76 | let mut from = self.from.borrow_mut(); 77 | let mut iteration = HashMap::new(); 78 | 79 | let finished = 80 | if self.step > T::default() { // Step is positive. 81 | *from >= self.to 82 | } else { // Step is negative. 83 | *from <= self.to 84 | }; 85 | 86 | let next = if finished { 87 | None 88 | } else { 89 | let value = from.clone(); 90 | *from = from.clone() + self.step.clone(); 91 | Some(value) 92 | }; 93 | 94 | keys::FINISHED.with( 95 | |finished| iteration.insert(finished.copy(), next.is_none().into()) 96 | ); 97 | 98 | if let Some(next) = next { 99 | keys::VALUE.with( 100 | |value| iteration.insert(value.copy(), next.into()) 101 | ); 102 | } 103 | 104 | Ok(Dict::new(iteration).into()) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/runtime/lib/read.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Read) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Read; 18 | 19 | 20 | impl Read { 21 | fn read() -> io::Result { 22 | let mut input = String::new(); 23 | 24 | io::stdin() 25 | .read_line(&mut input) 26 | .map(|_| input.into()) 27 | } 28 | } 29 | 30 | 31 | impl NativeFun for Read { 32 | fn name(&self) -> &'static str { "std.read" } 33 | 34 | fn call(&self, context: CallContext) -> Result { 35 | match context.args() { 36 | [ ] => Self::read() 37 | .map_err(|error| Panic::io(error, context.pos)), 38 | 39 | [ Value::String(ref string) ] => { 40 | let stdout = io::stdout(); 41 | let mut stdout = stdout.lock(); 42 | 43 | stdout 44 | .write_all(string.as_ref()) 45 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 46 | 47 | stdout 48 | .flush() 49 | .map_err(|error| Panic::io(error, context.pos.copy()))?; 50 | 51 | Self::read() 52 | .map_err(|error| Panic::io(error, context.pos)) 53 | }, 54 | 55 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 56 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/runtime/lib/regex.rs: -------------------------------------------------------------------------------- 1 | use std::{rc::Rc, collections::HashMap, borrow::Cow}; 2 | 3 | use gc::{Finalize, Trace}; 4 | use regex::bytes::Regex; 5 | 6 | use super::{ 7 | Error, 8 | CallContext, 9 | Dict, 10 | RustFun, 11 | NativeFun, 12 | Panic, 13 | Str, 14 | Value, 15 | }; 16 | 17 | 18 | inventory::submit! { RustFun::from(StdRegex) } 19 | 20 | #[derive(Trace, Finalize)] 21 | struct StdRegex; 22 | 23 | impl StdRegex { 24 | fn build(pattern: &[u8]) -> Value { 25 | let pattern = match std::str::from_utf8(pattern) { 26 | Ok(pattern) => pattern, 27 | Err(error) => return Error::new("invalid regex".into(), error.to_string().into()).into(), 28 | }; 29 | 30 | let pattern = match Regex::new(pattern) { 31 | Ok(pattern) => Rc::new(pattern), 32 | Err(error) => return Error::new("invalid regex".into(), error.to_string().into()).into(), 33 | }; 34 | 35 | thread_local! { 36 | pub static MATCH: Value = "match".into(); 37 | pub static SPLIT: Value = "split".into(); 38 | pub static REPLACE: Value = "replace".into(); 39 | } 40 | 41 | let mut dict = HashMap::new(); 42 | 43 | MATCH.with( 44 | |name| dict.insert(name.copy(), RegexMatchImpl { pattern: pattern.clone() }.into()) 45 | ); 46 | 47 | SPLIT.with( 48 | |split| dict.insert(split.copy(), RegexSplitImpl { pattern: pattern.clone() }.into()) 49 | ); 50 | 51 | REPLACE.with( 52 | |replace| dict.insert(replace.copy(), RegexReplaceImpl { pattern: pattern.clone() }.into()) 53 | ); 54 | 55 | Dict::new(dict).into() 56 | } 57 | } 58 | 59 | impl NativeFun for StdRegex { 60 | fn name(&self) -> &'static str { "std.regex" } 61 | 62 | fn call(&self, context: CallContext) -> Result { 63 | match context.args() { 64 | [ Value::String(ref string) ] => Ok(Self::build(string.as_ref())), 65 | 66 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 67 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 68 | } 69 | } 70 | } 71 | 72 | 73 | #[derive(Finalize)] 74 | struct RegexMatchImpl { 75 | pattern: Rc, 76 | } 77 | 78 | /// RegexMatchImpl has no garbage-collected fields. 79 | unsafe impl Trace for RegexMatchImpl { 80 | gc::unsafe_empty_trace!(); 81 | } 82 | 83 | impl NativeFun for RegexMatchImpl { 84 | fn name(&self) -> &'static str { "std.regex" } 85 | 86 | fn call(&self, context: CallContext) -> Result { 87 | match context.args() { 88 | [ Value::String(ref string) ] => Ok(self.pattern.is_match(string.as_ref()).into()), 89 | 90 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 91 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 92 | } 93 | } 94 | } 95 | 96 | #[derive(Finalize)] 97 | struct RegexSplitImpl { 98 | pattern: Rc, 99 | } 100 | 101 | /// RegexSplitImpl has no garbage-collected fields. 102 | unsafe impl Trace for RegexSplitImpl { 103 | gc::unsafe_empty_trace!(); 104 | } 105 | 106 | impl NativeFun for RegexSplitImpl { 107 | fn name(&self) -> &'static str { "std.regex" } 108 | 109 | fn call(&self, context: CallContext) -> Result { 110 | match context.args() { 111 | [ Value::String(ref string) ] => Ok( 112 | self.pattern 113 | .split(string.as_ref()) 114 | .map(Str::from) 115 | .map(Value::from) 116 | .collect::>() 117 | .into() 118 | ), 119 | 120 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 121 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 122 | } 123 | } 124 | } 125 | 126 | #[derive(Finalize)] 127 | struct RegexReplaceImpl { 128 | pattern: Rc, 129 | } 130 | 131 | /// RegexReplaceImpl has no garbage-collected fields. 132 | unsafe impl Trace for RegexReplaceImpl { 133 | gc::unsafe_empty_trace!(); 134 | } 135 | 136 | impl NativeFun for RegexReplaceImpl { 137 | fn name(&self) -> &'static str { "std.regex" } 138 | 139 | fn call(&self, context: CallContext) -> Result { 140 | match context.args() { 141 | [ value @ Value::String(ref string), Value::String(ref replace) ] => Ok( 142 | match self.pattern.replace_all(string.as_ref(), replace.as_bytes()) { 143 | Cow::Borrowed(_) => value.copy(), 144 | Cow::Owned(value) => Str::from(value).into(), 145 | } 146 | ), 147 | 148 | [ Value::String(_), other ] | [ other, _ ] => { 149 | Err(Panic::type_error(other.copy(), "string", context.pos)) 150 | }, 151 | 152 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/runtime/lib/replace.rs: -------------------------------------------------------------------------------- 1 | use bstr::ByteSlice; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Str, 11 | Value, 12 | }; 13 | 14 | 15 | inventory::submit! { RustFun::from(Replace) } 16 | 17 | #[derive(Trace, Finalize)] 18 | struct Replace; 19 | 20 | impl NativeFun for Replace { 21 | fn name(&self) -> &'static str { "std.replace" } 22 | 23 | fn call(&self, context: CallContext) -> Result { 24 | match context.args() { 25 | [ Value::String(ref string), Value::String(ref pattern), Value::String(ref replace) ] => Ok( 26 | Str::from( 27 | string 28 | .as_bytes() 29 | .replace(pattern, replace) 30 | ).into() 31 | ), 32 | 33 | [ Value::String(_), Value::String(_), other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 34 | [ Value::String(_), other, _ ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 35 | [ other, _, _ ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 36 | 37 | args => Err(Panic::invalid_args(args.len() as u32, 3, context.pos)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/runtime/lib/sleep.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | NativeFun, 8 | RustFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit!{ RustFun::from(Sleep) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Sleep; 18 | 19 | impl NativeFun for Sleep { 20 | fn name(&self) -> &'static str { "std.sleep" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | match context.args() { 24 | [ Value::Int(i) ] if *i < 0 => Err(Panic::value_error(Value::Int(*i), "positive integer", context.pos)), 25 | 26 | [ Value::Int(i) ] => { 27 | std::thread::sleep(Duration::from_millis(*i as u64)); 28 | Ok(Value::default()) 29 | }, 30 | 31 | [ other ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 32 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/runtime/lib/sort.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | }; 10 | 11 | 12 | inventory::submit! { RustFun::from(Sort) } 13 | 14 | #[derive(Trace, Finalize)] 15 | struct Sort; 16 | 17 | impl NativeFun for Sort { 18 | fn name(&self) -> &'static str { "std.sort" } 19 | 20 | fn call(&self, mut context: CallContext) -> Result { 21 | match context.args_mut() { 22 | [ Value::Array(ref mut array) ] => { 23 | array.sort(); 24 | Ok(Value::default()) 25 | } 26 | 27 | [ other ] => Err(Panic::type_error(other.copy(), "array", context.pos)), 28 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime/lib/split.rs: -------------------------------------------------------------------------------- 1 | use bstr::ByteSlice; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Split) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Split; 18 | 19 | impl NativeFun for Split { 20 | fn name(&self) -> &'static str { "std.split" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | match context.args() { 24 | [ Value::String(ref string), Value::String(ref pattern) ] => Ok( 25 | string 26 | .as_bytes() 27 | .split_str(pattern) 28 | .map(Value::from) 29 | .collect::>() 30 | .into() 31 | ), 32 | 33 | [ Value::String(_), other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 34 | [ other, _ ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 35 | 36 | args => Err(Panic::invalid_args(args.len() as u32, 2, context.pos)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/runtime/lib/substr.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | Error, 10 | }; 11 | 12 | 13 | inventory::submit! { RustFun::from(Substr) } 14 | 15 | #[derive(Trace, Finalize)] 16 | struct Substr; 17 | 18 | impl Substr { 19 | fn substr(string: &[u8], start: i64, len: i64) -> Result { 20 | let start = start as usize; 21 | let end = start + (len as usize); 22 | let substr = string.get(start..end); 23 | Ok(substr.into()) 24 | } 25 | } 26 | 27 | impl NativeFun for Substr { 28 | fn name(&self) -> &'static str { "std.substr" } 29 | 30 | fn call(&self, context: CallContext) -> Result { 31 | match context.args() { 32 | [ Value::String(ref string), Value::Int(start), Value::Int(len) ] => { 33 | // TODO Panic if indexes out of bounds 34 | let result = Self::substr(string.as_ref(), *start, *len); 35 | Ok(result.unwrap_or_else(Into::into)) 36 | }, 37 | [ other, Value::Int(_), Value::Int(_) ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 38 | [ Value::String(_), other, Value::Int(_) ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 39 | [ Value::String(_), Value::Int(_), other ] => Err(Panic::type_error(other.copy(), "int", context.pos)), 40 | args => Err(Panic::invalid_args(args.len() as u32, 3, context.pos)) 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/runtime/lib/to_string.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use crate::fmt::FmtString; 4 | use super::{ 5 | CallContext, 6 | RustFun, 7 | NativeFun, 8 | Panic, 9 | Value, 10 | }; 11 | 12 | 13 | inventory::submit! { RustFun::from(ToString) } 14 | 15 | #[derive(Trace, Finalize)] 16 | struct ToString; 17 | 18 | impl NativeFun for ToString { 19 | fn name(&self) -> &'static str { "std.to_string" } 20 | 21 | fn call(&self, context: CallContext) -> Result { 22 | match context.args() { 23 | [ Value::String(ref string) ] => Ok(string.copy().into()), 24 | [ value ] => Ok(value.fmt_string(context.interner()).into()), 25 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/runtime/lib/trim.rs: -------------------------------------------------------------------------------- 1 | use bstr::ByteSlice; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | }; 12 | 13 | 14 | inventory::submit! { RustFun::from(Trim) } 15 | 16 | #[derive(Trace, Finalize)] 17 | struct Trim; 18 | 19 | impl NativeFun for Trim { 20 | fn name(&self) -> &'static str { "std.trim" } 21 | 22 | fn call(&self, context: CallContext) -> Result { 23 | match context.args() { 24 | [ Value::String(ref string) ] => Ok( 25 | string 26 | .as_bytes() 27 | .trim() 28 | .into() 29 | ), 30 | 31 | [ other ] => Err(Panic::type_error(other.copy(), "string", context.pos)), 32 | 33 | args => Err(Panic::invalid_args(args.len() as u32, 1, context.pos)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/runtime/lib/type_.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use super::{ 4 | CallContext, 5 | RustFun, 6 | NativeFun, 7 | Panic, 8 | Value, 9 | Type, 10 | }; 11 | 12 | 13 | inventory::submit! { RustFun::from(StdType) } 14 | 15 | #[derive(Trace, Finalize)] 16 | pub struct StdType; 17 | 18 | 19 | impl StdType { 20 | /// Get the type string as a value. 21 | pub fn get_type(value: &Value) -> Value { 22 | thread_local! { 23 | pub static NIL: Value = Type::Nil.display().into(); 24 | pub static BOOL: Value = Type::Bool.display().into(); 25 | pub static INT: Value = Type::Int.display().into(); 26 | pub static FLOAT: Value = Type::Float.display().into(); 27 | pub static BYTE: Value = Type::Byte.display().into(); 28 | pub static STRING: Value = Type::String.display().into(); 29 | pub static ARRAY: Value = Type::Array.display().into(); 30 | pub static DICT: Value = Type::Dict.display().into(); 31 | pub static FUNCTION: Value = Type::Function.display().into(); 32 | pub static ERROR: Value = Type::Error.display().into(); 33 | } 34 | 35 | let typename = match value { 36 | Value::Nil => &NIL, 37 | Value::Bool(_) => &BOOL, 38 | Value::Int(_) => &INT, 39 | Value::Float(_) => &FLOAT, 40 | Value::Byte(_) => &BYTE, 41 | Value::String(_) => &STRING, 42 | Value::Array(_) => &ARRAY, 43 | Value::Dict(_) => &DICT, 44 | Value::Function(_) => &FUNCTION, 45 | Value::Error(_) => &ERROR, 46 | }; 47 | 48 | typename.with(Value::copy) 49 | } 50 | } 51 | 52 | 53 | impl NativeFun for StdType { 54 | fn name(&self) -> &'static str { "std.type" } 55 | 56 | fn call(&self, context: CallContext) -> Result { 57 | match context.args() { 58 | [ value ] => Ok(Self::get_type(value)), 59 | 60 | args @ [] | args @ [_, _, ..] => Err( 61 | Panic::invalid_args(args.len() as u32, 1, context.pos) 62 | ), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/runtime/lib/typecheck.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | 3 | use crate::fmt; 4 | 5 | use super::{ 6 | CallContext, 7 | RustFun, 8 | NativeFun, 9 | Panic, 10 | Value, 11 | Type, 12 | Error, 13 | }; 14 | 15 | 16 | inventory::submit! { RustFun::from(Typecheck) } 17 | inventory::submit! { RustFun::from(TryTypecheck) } 18 | 19 | 20 | /// A typecheck result. 21 | enum TypeChecked { 22 | Valid(Value), 23 | Invalid { 24 | value: Value, 25 | expected_type: Type, 26 | } 27 | } 28 | 29 | 30 | #[derive(Trace, Finalize)] 31 | struct Typecheck; 32 | 33 | 34 | impl Typecheck { 35 | fn typecheck(context: &CallContext) -> Result { 36 | match context.args() { 37 | [ value, Value::String(expected) ] => { 38 | let expected_type = Type 39 | ::parse(expected) 40 | .ok_or_else( 41 | || Panic::value_error( 42 | Value::String(expected.copy()), 43 | "valid type", 44 | context.pos.copy() 45 | ) 46 | )?; 47 | 48 | let value = value.copy(); 49 | 50 | Ok( 51 | if value.get_type() == expected_type { 52 | TypeChecked::Valid(value) 53 | } 54 | else { 55 | TypeChecked::Invalid { value, expected_type } 56 | } 57 | ) 58 | } 59 | 60 | [ _, other ] => Err(Panic::type_error(other.copy(), "string", context.pos.copy())), 61 | args @ [] | args @ [_] | args @ [_, _, ..] => Err( 62 | Panic::invalid_args(args.len() as u32, 2, context.pos.copy()) 63 | ), 64 | } 65 | } 66 | } 67 | 68 | 69 | impl NativeFun for Typecheck { 70 | fn name(&self) -> &'static str { "std.typecheck" } 71 | 72 | fn call(&self, context: CallContext) -> Result { 73 | match Self::typecheck(&context)? { 74 | // No problem in returning the value here, as typecheck errors are signaled as panics. 75 | TypeChecked::Valid(value) => Ok(value), 76 | 77 | TypeChecked::Invalid { value, expected_type } => Err( 78 | Panic::type_error( 79 | value, 80 | expected_type.display(), 81 | context.pos 82 | ) 83 | ), 84 | } 85 | } 86 | } 87 | 88 | 89 | #[derive(Trace, Finalize)] 90 | struct TryTypecheck; 91 | 92 | 93 | impl NativeFun for TryTypecheck { 94 | fn name(&self) -> &'static str { "std.try_typecheck" } 95 | 96 | fn call(&self, context: CallContext) -> Result { 97 | match Typecheck::typecheck(&context)? { 98 | // We can't return the value here, because it would be impossible to distinguish a 99 | // typechecked error value from a type error. 100 | TypeChecked::Valid(_) => Ok(Value::default()), 101 | 102 | TypeChecked::Invalid { value, expected_type } => { 103 | let description = format!( 104 | "type error: expected {}, got {} ({})", 105 | expected_type, 106 | value.get_type(), 107 | fmt::Show(&value, context.interner()), 108 | ); 109 | 110 | Ok( 111 | Error 112 | ::new(description.into(), value) 113 | .into() 114 | ) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/runtime/lib/util.rs: -------------------------------------------------------------------------------- 1 | use super::{Float, Value}; 2 | 3 | 4 | /// A triple of numbers promoted to the same type. 5 | #[derive(Debug)] 6 | pub enum Numbers { 7 | Ints([i64; N]), 8 | Floats([Float; N]), 9 | } 10 | 11 | 12 | impl Numbers { 13 | /// Promote numbers to float if necessary. 14 | pub fn promote(values: [Value; N]) -> Result { 15 | let mut numbers = Numbers::Ints([0; N]); 16 | 17 | for ix in 0..N { 18 | match (&mut numbers, &values[ix]) { 19 | (Numbers::Ints(ints), Value::Int(int)) => ints[ix] = *int, 20 | 21 | (numbers @ Numbers::Ints(_), Value::Float(float)) => { 22 | let floats = numbers.as_floats(); 23 | floats[ix] = float.copy(); 24 | } 25 | 26 | (Numbers::Floats(floats), Value::Int(int)) => floats[ix] = int.into(), 27 | 28 | (Numbers::Floats(floats), Value::Float(float)) => floats[ix] = float.copy(), 29 | 30 | (_, value) => return Err(value.copy()), 31 | } 32 | } 33 | 34 | Ok(numbers) 35 | } 36 | 37 | 38 | fn as_floats(&mut self) -> &mut [Float; N] { 39 | match self { 40 | Numbers::Ints(ints) => { 41 | const ZERO: Float = Float(0.0); 42 | let mut floats = [ZERO; N]; 43 | 44 | for ix in 0..N { 45 | floats[ix] = ints[ix].into(); 46 | } 47 | 48 | *self = Numbers::Floats(floats); 49 | 50 | self.as_floats() 51 | } 52 | 53 | Numbers::Floats(floats) => floats, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/runtime/source.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use crate::{ 4 | fmt::{self, Display}, 5 | syntax, 6 | symbol::{self, Symbol}, 7 | }; 8 | 9 | use gc::{Finalize, Trace}; 10 | 11 | 12 | /// A human readable position in the source code. 13 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 14 | #[derive(Finalize)] 15 | pub struct SourcePos { 16 | pub line: u32, 17 | pub column: u32, 18 | pub path: Symbol, 19 | } 20 | 21 | 22 | impl SourcePos { 23 | /// Shallow copy. 24 | pub fn copy(&self) -> Self { 25 | Self { .. *self } 26 | } 27 | 28 | 29 | /// Create a new SourcePos refering to the beginning of the file. 30 | pub fn file(path: Symbol) -> Self { 31 | Self { line: 0, column: 0, path } 32 | } 33 | } 34 | 35 | 36 | /// SourcePos has no garbage-collected fields. 37 | unsafe impl Trace for SourcePos { 38 | gc::unsafe_empty_trace!(); 39 | } 40 | 41 | 42 | impl PartialOrd for SourcePos { 43 | fn partial_cmp(&self, other: &Self) -> Option { 44 | Some(self.cmp(other)) 45 | } 46 | } 47 | 48 | 49 | impl Ord for SourcePos { 50 | fn cmp(&self, other: &Self) -> Ordering { 51 | (usize::from(self.path), self.line, self.column) 52 | .cmp(&(other.path.into(), other.line, other.column)) 53 | } 54 | } 55 | 56 | 57 | impl From for SourcePos { 58 | fn from(pos: syntax::SourcePos) -> Self { 59 | Self { 60 | line: pos.line, 61 | column: pos.column, 62 | path: pos.path, 63 | } 64 | } 65 | } 66 | 67 | 68 | impl<'a> From<&'a syntax::SourcePos> for SourcePos { 69 | fn from(pos: &'a syntax::SourcePos) -> Self { 70 | (*pos).into() 71 | } 72 | } 73 | 74 | 75 | impl<'a> Display<'a> for SourcePos { 76 | type Context = &'a symbol::Interner; 77 | 78 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 79 | write!( 80 | f, 81 | "{} (line {}, column {})", 82 | fmt::Show(self.path, context), 83 | self.line, 84 | self.column 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/asserts/assert.hsh: -------------------------------------------------------------------------------- 1 | std.assert(false) 2 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/asserts/closures.hsh: -------------------------------------------------------------------------------- 1 | function fun() 2 | let x = 0 3 | 4 | let fun = function() 5 | x 6 | end 7 | 8 | x = 1 9 | return fun 10 | end 11 | 12 | std.assert(fun()() == 0) 13 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/asserts/iter-fun.hsh: -------------------------------------------------------------------------------- 1 | function once(value) 2 | let finished = false 3 | function () 4 | if finished then 5 | @[ finished: false ] 6 | else 7 | finished = true 8 | @[ finished: false, value: value ] 9 | end 10 | end 11 | end 12 | 13 | for _ in once(1) do 14 | std.assert(false) 15 | end 16 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/asserts/self.hsh: -------------------------------------------------------------------------------- 1 | let obj = @[ 2 | test: function (arg) 3 | self 4 | end, 5 | ] 6 | 7 | std.assert(obj.test(1) == 1) 8 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/asserts/type.hsh: -------------------------------------------------------------------------------- 1 | let type = std.type(function() 1 end()) 2 | std.assert(type == "function") 3 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/assign-error-field.hsh: -------------------------------------------------------------------------------- 1 | let error = std.error("something bad happened", nil) 2 | error.context = "oops..." 3 | -------------------------------------------------------------------------------- /src/runtime/tests/data/negative/compare-int-float.hsh: -------------------------------------------------------------------------------- 1 | let x = 1 2 | let y = 2.0 3 | let result = x > y 4 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/assert.hsh: -------------------------------------------------------------------------------- 1 | std.assert(true) 2 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/bind.hsh: -------------------------------------------------------------------------------- 1 | function orphan(arg1, arg2) 2 | self ++ arg1 ++ arg2 3 | end 4 | 5 | let obj = "foo" 6 | let fun = std.bind(obj, orphan) 7 | 8 | std.assert(fun("bar", "baz") == "foobarbaz") 9 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/capture-large.hsh: -------------------------------------------------------------------------------- 1 | let size = 1024 * 1024 # 1 MB 2 | 3 | let result = ${ 4 | head -c $size /dev/zero; 5 | head -c $size /dev/zero 6 | } 7 | 8 | std.assert(std.len(result.stdout) == (size * 2)) 9 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/capture.hsh: -------------------------------------------------------------------------------- 1 | let result = ${ 2 | echo 321 | rev | cat; 3 | echo 456 4 | } 5 | 6 | std.assert(result.stdout == "123\n456\n") 7 | std.assert(result.stderr == "") 8 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/catch.hsh: -------------------------------------------------------------------------------- 1 | let catch = std.catch 2 | let typecheck = std.typecheck 3 | 4 | function assert_caught(fn) 5 | let result = catch(fn) 6 | typecheck(result, "error") 7 | end 8 | 9 | assert_caught( 10 | function() 11 | 1 > 2.0 12 | end 13 | ) 14 | 15 | assert_caught( 16 | function() 17 | let error = std.error("something bad happened", nil) 18 | error.context = "oops..." 19 | end 20 | ) 21 | 22 | assert_caught( 23 | function() 24 | 1 / 0 25 | end 26 | ) 27 | 28 | assert_caught( 29 | function() 30 | [][5] 31 | end 32 | ) 33 | 34 | assert_caught( 35 | function() 36 | std.assert() 37 | end 38 | ) 39 | 40 | assert_caught( 41 | function() 42 | std.assert(false) 43 | end 44 | ) 45 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/closures.hsh: -------------------------------------------------------------------------------- 1 | function fun() 2 | let x = 0 3 | 4 | let fun = function() 5 | let fun = function() 6 | return x 7 | end 8 | 9 | x = 2 10 | return fun 11 | end 12 | 13 | x = 1 14 | return fun 15 | end 16 | 17 | std.assert(fun()()() == 2) 18 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/command-bail.hsh: -------------------------------------------------------------------------------- 1 | let result = { 2 | echo foo | cat | false | cat | cat > /dev/null; 3 | echo baz 4 | } 5 | 6 | std.assert(std.type(result) == "error") 7 | std.assert(std.len(result.context) == 2) # false and the subsequent cat should fail. 8 | 9 | for error in std.iter(result.context) do 10 | std.assert(std.type(error) == "error") 11 | std.assert(error.description == "command returned non-zero") 12 | end 13 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/contains.hsh: -------------------------------------------------------------------------------- 1 | let dict = @[ 2 | hei: "you", 3 | out: "there", 4 | on: "the", 5 | wall: "..." 6 | ] 7 | 8 | std.assert(std.contains(dict, "hei")) 9 | std.assert(not std.contains(dict, "foo")) 10 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/dict-iter.hsh: -------------------------------------------------------------------------------- 1 | let arr = @[ one: "two", three: 4, five: [ "six" ], seven: '8' ] 2 | let types = @[ one: "string", three: "int", five: "array", seven: "char" ] 3 | 4 | for item in std.iter(arr) do 5 | std.assert(std.type(item.key) == "string") 6 | std.assert(std.type(item.value) == types[item.key]) 7 | end 8 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/env.hsh: -------------------------------------------------------------------------------- 1 | function run() 2 | let value = "a distinguished value" 3 | let result = ${ my_env=$value env }? 4 | 5 | let env = std.split(result.stdout, "\n") 6 | 7 | std.contains(env, "my_env=a distinguished value") 8 | end 9 | 10 | std.assert(run() == true) 11 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/factorial.hsh: -------------------------------------------------------------------------------- 1 | function factorial(n) 2 | if n < 2 then 3 | return 1 4 | else 5 | return n * factorial(n - 1) 6 | end 7 | end 8 | 9 | std.assert(factorial(5) == 120) 10 | std.assert(factorial(0) == 1) 11 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/float.hsh: -------------------------------------------------------------------------------- 1 | std.assert(std.float(1) == 1.0) 2 | std.assert(std.float(1.0) == 1.0) 3 | std.assert(std.float("1") == 1.0) 4 | std.assert(std.float("1.") == 1.0) 5 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/int-float-hash.hsh: -------------------------------------------------------------------------------- 1 | let dict = @[ ] 2 | dict[1] = true 3 | dict[1.0] = false 4 | 5 | std.assert(std.len(dict) == 2) 6 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/int.hsh: -------------------------------------------------------------------------------- 1 | std.assert(std.int(1) == 1) 2 | std.assert(std.int(1.0) == 1) 3 | std.assert(std.int("1") == 1) 4 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/iter-fun.hsh: -------------------------------------------------------------------------------- 1 | for _ in function () @[ finished: true ] end do 2 | std.assert(false) 3 | end 4 | 5 | 6 | let ix = 0 7 | 8 | function count_to_five() 9 | if ix == 5 then 10 | return @[ finished: true ] 11 | end 12 | 13 | let result = @[ finished: false, value: ix ] 14 | ix = ix + 1 15 | result 16 | end 17 | 18 | let count = 0 19 | for i in count_to_five do 20 | std.assert(i == count) 21 | count = count + 1 22 | end 23 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/iter-lib.hsh: -------------------------------------------------------------------------------- 1 | let Iter = std.import("../../../../../examples/hush/iterator.hsh") 2 | 3 | let arr = [0, 5, 2, 4, 3, 1, 6] 4 | 5 | let id = function (i) 6 | std.assert(i == 2) 7 | i 8 | end 9 | 10 | let iter = Iter.Array(arr) 11 | .skip(1) 12 | .take(5) 13 | .map(id) 14 | 15 | std.assert(iter.nth(1).value == 2) 16 | 17 | iter = Iter.Array(arr) 18 | .skip(1) 19 | .take(5) 20 | .sorted(nil) 21 | 22 | std.assert(iter.collect(nil) == [ 1, 2, 3, 4, 5 ]) 23 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/pipeline.hsh: -------------------------------------------------------------------------------- 1 | let animals = "large white cat 2 | medium black cat 3 | big yellow dog 4 | small yellow cat 5 | small white dog 6 | medium green turtle" 7 | 8 | let result = ${ cat << $animals | grep dog | cut -d ' ' -f 2 | tr a-z A-Z }.stdout 9 | 10 | std.assert(result == "YELLOW\nWHITE\n") 11 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/print.hsh: -------------------------------------------------------------------------------- 1 | # NB: These paths assume this script is run from the root 2 | # directory of the git repo ($HOME/Code/hush or whatever) 3 | 4 | let THIS_SCRIPT = "./src/runtime/tests/data/positive/print.hsh" 5 | 6 | if std.env("FOO_HSH") == "1" then 7 | let nerp = nil 8 | 9 | let t = 1 == 1 10 | let f = 1 == 2 11 | 12 | let i = 10001 13 | let j = 20202 14 | 15 | let pi = 3.14159 16 | let e = 2.71828 17 | 18 | let a = 'a' 19 | let b = 'b' 20 | 21 | let h = "hello" 22 | let w = "world" 23 | 24 | let a1 = [1, 2] 25 | let a2 = [[11, 12], [21, 22]] 26 | 27 | let d1 = @[ foo: "bar" ] 28 | # TODO: Dictionary print order is not constant. Can't do simple test 29 | # of print results. 30 | #let d2 = @[ moo: "goo", gai: "pan"] 31 | let d2 = @[ moo: "goo"] 32 | 33 | let f1 = function () end 34 | let f2 = std.env 35 | 36 | let efail = std.error("EFAIL", nil) 37 | 38 | std.print("Nulls:", nil, nerp) 39 | std.println() 40 | 41 | std.print("Bools:", true, false, t, f) 42 | std.println() 43 | 44 | std.print("Ints:", 0, 1, 2, i, j) 45 | std.println() 46 | 47 | std.print("Reals:", 0.0, 1.0, 6.02E23, pi, e) 48 | std.println() 49 | 50 | std.print("Chars:", a, b, 'c', 'd', 'e') 51 | std.println() 52 | 53 | std.print("Strings:", "the quick brown fox", "jumped over", h, w) 54 | std.println() 55 | 56 | std.print("Arrays:", [0, 0], a1, a2) 57 | std.println() 58 | 59 | std.print("Dicts:", @[ d: "ict"], d1, d2) 60 | std.println() 61 | 62 | std.print("Funcs:", f1, f2) 63 | std.println() 64 | 65 | std.print("Error:", efail) 66 | std.println() 67 | 68 | std.print("All together now:", "all") 69 | std.print(" together") 70 | std.print(" now!") 71 | std.print('\n') 72 | 73 | std.exit(0) 74 | else 75 | std.export("FOO_HSH", "1") 76 | let result = ${ cargo run $THIS_SCRIPT }.stdout 77 | let lines = std.split(result, "\n") 78 | 79 | let expected = [ 80 | "Nulls:\tnil\tnil", 81 | "Bools:\ttrue\tfalse\ttrue\tfalse", 82 | "Ints:\t0\t1\t2\t10001\t20202", 83 | "Reals:\t0.0\t1.0\t6.02e23\t3.14159\t2.71828", 84 | "Chars:\ta\tb\tc\td\te", 85 | "Strings:\tthe quick brown fox\tjumped over\thello\tworld", 86 | "Arrays:\t[ 0, 0 ]\t[ 1, 2 ]\t[ [ 11, 12 ], [ 21, 22 ] ]", 87 | # TODO: Dictionary print order not constant. 88 | # "Dicts:\t@[ \"d\": \"ict\" ]\t@[ \"foo\": \"bar\" ]\t@[ \"moo\": \"goo\", \"gai\": \"pan\" ]", 89 | "Dicts:\t@[ \"d\": \"ict\" ]\t@[ \"foo\": \"bar\" ]\t@[ \"moo\": \"goo\" ]", 90 | ### NOTE: REGEX BELOW ("Funcs") 91 | "Funcs:\tfunction<[^>]*>\tstd.env", 92 | "Error:\terror: \"EFAIL\" (nil)", 93 | "All together now:\tall together now!" 94 | ] 95 | 96 | let i = 0 97 | for want in std.iter(expected) do 98 | if std.substr(lines[i], 0, 5) == "Funcs" then 99 | # Match instead of compare 100 | let rex = std.regex(want) 101 | if not rex.match(lines[i]) then 102 | std.println("Error: wanted /" ++ want 103 | ++ "/, got >>" ++ lines[i] ++ "<<") 104 | std.assert(false) 105 | end 106 | else 107 | if lines[i] != want then 108 | std.println("Error: wanted >>" ++ want 109 | ++ "<< got >>" ++ lines[i] ++ "<<") 110 | std.assert(false) 111 | end 112 | end 113 | i = i + 1 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/redirections.hsh: -------------------------------------------------------------------------------- 1 | let result = ${ 2 | src/runtime/tests/data/stdout-stderr.sh 3 | } 4 | 5 | std.assert(result.stdout == "stdout\n") 6 | std.assert(result.stderr == "stderr\n") 7 | 8 | result = ${ 9 | src/runtime/tests/data/stdout-stderr.sh 2>1 > /dev/null 10 | } 11 | 12 | std.assert(result.stdout == "stderr\n") 13 | 14 | 15 | result = ${ 16 | src/runtime/tests/data/stdout-stderr.sh 1>2 2> /dev/null 17 | } 18 | 19 | std.assert(result.stderr == "stdout\n") 20 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/self.hsh: -------------------------------------------------------------------------------- 1 | function orphan() 2 | self 3 | end 4 | 5 | std.assert(orphan() == nil) 6 | 7 | 8 | function fun() 9 | return @[ 10 | x: "Hello world!", 11 | 12 | store_foo: function () 13 | self.x = "Foo" 14 | end, 15 | 16 | store_bar: function () 17 | self.x = "Bar" 18 | end, 19 | 20 | fetch: function () 21 | self.x 22 | end, 23 | ] 24 | end 25 | 26 | 27 | let obj = fun() 28 | 29 | std.assert(obj.fetch() == "Hello world!") 30 | obj.store_foo() 31 | std.assert(obj.fetch() == "Foo") 32 | obj.store_bar() 33 | std.assert(obj.fetch() == "Bar") 34 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/sort.hsh: -------------------------------------------------------------------------------- 1 | let array = [ 15, 2, 6, 2.0 ] 2 | std.sort(array) 3 | std.assert(array == [ 2, 6, 15, 2.0 ]) 4 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/split.hsh: -------------------------------------------------------------------------------- 1 | std.assert( 2 | std.split("a b c d", " ") == [ "a", "b c d" ] 3 | ) 4 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/try.hsh: -------------------------------------------------------------------------------- 1 | function oops(ill) 2 | if ill then 3 | std.error("something went wrong", nil) 4 | else 5 | @[ 6 | message: "it works!", 7 | value: 0 8 | ] 9 | end 10 | end 11 | 12 | 13 | function foo() 14 | let first = oops(false)?.message 15 | std.assert(std.type(first) == "string") 16 | 17 | let second = oops(true)? 18 | std.assert(false) 19 | end 20 | 21 | 22 | let result = foo() 23 | std.assert(std.type(result) == "error") 24 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/type.hsh: -------------------------------------------------------------------------------- 1 | std.assert(std.type(nil) == "nil") 2 | std.assert(std.type(true) == "bool") 3 | std.assert(std.type(1) == "int") 4 | std.assert(std.type(1.0) == "float") 5 | std.assert(std.type('\0') == "char") 6 | std.assert(std.type("\n") == "string") 7 | std.assert(std.type([]) == "array") 8 | std.assert(std.type(@[]) == "dict") 9 | std.assert(std.type(function () end) == "function") 10 | std.assert(std.type(std.error("error", nil)) == "error") 11 | -------------------------------------------------------------------------------- /src/runtime/tests/data/positive/typecheck.hsh: -------------------------------------------------------------------------------- 1 | std.typecheck(nil, "nil") 2 | std.typecheck(true, "bool") 3 | std.typecheck(1, "int") 4 | std.typecheck(1.0, "float") 5 | std.typecheck('\0', "char") 6 | std.typecheck("\n", "string") 7 | std.typecheck([], "array") 8 | std.typecheck(@[], "dict") 9 | std.typecheck(function () end, "function") 10 | std.typecheck(std.typecheck, "function") 11 | std.typecheck(std.error("error", nil), "error") 12 | 13 | 14 | function assert_try_typecheck_result(value, expected_type, type_error_expected) 15 | let result = std.try_typecheck(value, expected_type) 16 | 17 | if type_error_expected then 18 | std.assert(std.type(result) == "error") 19 | else 20 | std.assert(result == nil) 21 | end 22 | end 23 | 24 | assert_try_typecheck_result(nil, "nil", false) 25 | assert_try_typecheck_result(true, "bool", false) 26 | assert_try_typecheck_result(1, "int", false) 27 | assert_try_typecheck_result(1.0, "float", false) 28 | assert_try_typecheck_result('\0', "char", false) 29 | assert_try_typecheck_result("\n", "string", false) 30 | assert_try_typecheck_result([], "array", false) 31 | assert_try_typecheck_result(@[], "dict", false) 32 | assert_try_typecheck_result(function () end, "function", false) 33 | assert_try_typecheck_result(std.typecheck, "function", false) 34 | assert_try_typecheck_result(std.error("error", nil), "error", false) 35 | 36 | assert_try_typecheck_result(nil, "bool", true) 37 | assert_try_typecheck_result(true, "nil", true) 38 | assert_try_typecheck_result(1, "float", true) 39 | assert_try_typecheck_result(1.0, "int", true) 40 | assert_try_typecheck_result('\0', "string", true) 41 | assert_try_typecheck_result("\n", "char", true) 42 | assert_try_typecheck_result([], "function", true) 43 | assert_try_typecheck_result(@[], "error", true) 44 | assert_try_typecheck_result(function () end, "array", true) 45 | assert_try_typecheck_result(std.error("error", nil), "dict", true) 46 | -------------------------------------------------------------------------------- /src/runtime/tests/data/stdout-stderr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo stdout 4 | 5 | echo stderr 1>&2 6 | -------------------------------------------------------------------------------- /src/runtime/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | path::Path, 4 | os::unix::ffi::OsStrExt, 5 | }; 6 | 7 | use serial_test::serial; 8 | 9 | use crate::{ 10 | fmt, 11 | semantic::{self, ErrorsDisplayContext}, 12 | symbol, 13 | syntax::{self, AnalysisDisplayContext}, 14 | tests, 15 | }; 16 | use super::{Runtime, Value, Panic}; 17 | 18 | 19 | fn test_dir(path: P, mut check: F) -> io::Result<()> 20 | where 21 | P: AsRef, 22 | F: FnMut(&Result) -> bool, 23 | { 24 | let interner = symbol::Interner::new(); 25 | let args = std::iter::empty::<&str>(); 26 | let mut runtime = Runtime::new(args, interner); 27 | 28 | tests::util::test_dir( 29 | path, 30 | move |path, file| { 31 | let path_symbol = runtime 32 | .interner_mut() 33 | .get_or_intern(path.as_os_str().as_bytes()); 34 | let source = syntax::Source::from_reader(path_symbol, file)?; 35 | let syntactic_analysis = syntax::Analysis::analyze( 36 | &source, 37 | runtime.interner_mut() 38 | ); 39 | 40 | if !syntactic_analysis.errors.is_empty() { 41 | panic!( 42 | "{}", 43 | fmt::Show( 44 | syntactic_analysis, 45 | AnalysisDisplayContext { 46 | max_errors: None, 47 | interner: runtime.interner(), 48 | } 49 | ) 50 | ); 51 | } 52 | 53 | let semantic_analysis = semantic::Analyzer::analyze( 54 | syntactic_analysis.ast, 55 | runtime.interner_mut() 56 | ); 57 | let program = match semantic_analysis { 58 | Ok(program) => program, 59 | Err(errors) => panic!( 60 | "{}", 61 | fmt::Show( 62 | errors, 63 | ErrorsDisplayContext { 64 | max_errors: None, 65 | interner: runtime.interner(), 66 | } 67 | ) 68 | ), 69 | }; 70 | 71 | let program = Box::leak(Box::new(program)); 72 | 73 | let result = runtime.eval(program); 74 | 75 | if !check(&result) { 76 | match result { 77 | Ok(value) => panic!( 78 | "File {}: expected panic, got {}", 79 | path.display(), 80 | fmt::Show(value, runtime.interner()) 81 | ), 82 | Err(panic) => panic!("{}", fmt::Show(panic, runtime.interner())), 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | ) 89 | } 90 | 91 | 92 | // As our garbage collector is not thread safe, we must *not* run the following tests in 93 | // parallel. 94 | 95 | 96 | #[test] 97 | #[serial] 98 | fn test_positive() -> io::Result<()> { 99 | test_dir( 100 | "src/runtime/tests/data/positive", 101 | Result::is_ok 102 | ) 103 | } 104 | 105 | 106 | #[test] 107 | #[serial] 108 | fn test_negative() -> io::Result<()> { 109 | test_dir( 110 | "src/runtime/tests/data/negative", 111 | Result::is_err 112 | ) 113 | } 114 | 115 | 116 | #[test] 117 | #[serial] 118 | fn test_asserts() -> io::Result<()> { 119 | test_dir( 120 | "src/runtime/tests/data/negative/asserts", 121 | |result| matches!(result, Err(Panic::AssertionFailed { .. })) 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/runtime/value/array.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | hash::{Hash, Hasher}, 4 | ops::Deref, 5 | }; 6 | 7 | use gc::{Gc, GcCell, GcCellRef, GcCellRefMut, Finalize, Trace}; 8 | 9 | use super::{EmptyCollection, IndexOutOfBounds, Value}; 10 | 11 | 12 | /// An array in the language. 13 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 14 | #[derive(Trace, Finalize)] 15 | pub struct Array(Gc>>); 16 | 17 | 18 | impl Array { 19 | /// Crate a new empty array. 20 | pub fn new(vec: Vec) -> Self { 21 | Self(Gc::new(GcCell::new(vec))) 22 | } 23 | 24 | 25 | /// Shallow copy. 26 | pub fn copy(&self) -> Self { 27 | Self(self.0.clone()) 28 | } 29 | 30 | 31 | /// Borrow the inner Vec. 32 | pub fn borrow(&self) -> GcCellRef> { 33 | self.0.deref().borrow() 34 | } 35 | 36 | 37 | /// Borrow the inner Vec mutably. 38 | pub fn borrow_mut(&self) -> GcCellRefMut> { 39 | self.0.deref().borrow_mut() 40 | } 41 | 42 | 43 | /// Push a value into the array. 44 | pub fn push(&mut self, value: Value) { 45 | self.0.borrow_mut().push(value) 46 | } 47 | 48 | 49 | /// Pop a value from the back of the array. 50 | pub fn pop(&mut self) -> Result { 51 | self.0 52 | .borrow_mut() 53 | .pop() 54 | .ok_or(EmptyCollection) 55 | } 56 | 57 | 58 | /// Get the value at a given index. 59 | pub fn index(&self, index: i64) -> Result { 60 | let index: usize = index 61 | .try_into() 62 | .map_err(|_| IndexOutOfBounds)?; 63 | 64 | self 65 | .borrow() 66 | .get(index) 67 | .map(Value::copy) 68 | .ok_or(IndexOutOfBounds) 69 | } 70 | 71 | 72 | /// Check if the collections contains the given value 73 | pub fn contains(&self, value: &Value) -> bool { 74 | self 75 | .borrow() 76 | .contains(value) 77 | } 78 | 79 | 80 | /// Assign a value to the given index. 81 | pub fn set(&self, index: i64, value: Value) -> Result<(), IndexOutOfBounds> { 82 | let index: usize = index 83 | .try_into() 84 | .map_err(|_| IndexOutOfBounds)?; 85 | 86 | let mut array = self.borrow_mut(); 87 | 88 | let val = array 89 | .get_mut(index) 90 | .ok_or(IndexOutOfBounds)?; 91 | 92 | *val = value; 93 | 94 | Ok(()) 95 | } 96 | 97 | 98 | /// Get the array length. 99 | pub fn len(&self) -> i64 { 100 | self.borrow().len() as i64 101 | } 102 | 103 | 104 | /// Whether the array is empty. 105 | pub fn is_empty(&self) -> bool { 106 | self.len() == 0 107 | } 108 | 109 | /// Stable sort 110 | pub fn sort(&mut self) { 111 | self.borrow_mut().sort(); 112 | } 113 | } 114 | 115 | 116 | // GcCell does not implement Eq because `borrow` might panic. 117 | #[allow(clippy::derived_hash_with_manual_eq)] 118 | impl Hash for Array { 119 | fn hash(&self, state: &mut H) { 120 | self.borrow().hash(state) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/runtime/value/dict.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | collections::{HashMap, BTreeMap}, 4 | hash::{Hash, Hasher}, 5 | ops::Deref, 6 | }; 7 | 8 | use gc::{Gc, GcCell, GcCellRef, GcCellRefMut, Finalize, Trace}; 9 | 10 | use super::{IndexOutOfBounds, Value}; 11 | 12 | 13 | /// Common dict keys 14 | pub mod keys { 15 | use super::Value; 16 | 17 | thread_local! { 18 | /// FINISHED string key. 19 | pub static FINISHED: Value = "finished".into(); 20 | /// FINISHED string key. 21 | pub static KEY: Value = "key".into(); 22 | /// VALUE string key. 23 | pub static VALUE: Value = "value".into(); 24 | } 25 | } 26 | 27 | 28 | /// A dict in the language. 29 | #[derive(Debug, Default, PartialEq, Eq)] 30 | #[derive(Trace, Finalize)] 31 | pub struct Dict(Gc>>); 32 | 33 | 34 | impl Dict { 35 | /// Crate a new empty dict. 36 | pub fn new(dict: HashMap) -> Self { 37 | Self(Gc::new(GcCell::new(dict))) 38 | } 39 | 40 | 41 | /// Shallow copy. 42 | pub fn copy(&self) -> Self { 43 | Self(self.0.clone()) 44 | } 45 | 46 | 47 | /// Borrow the hashmap. 48 | pub fn borrow(&self) -> GcCellRef> { 49 | self.0.deref().borrow() 50 | } 51 | 52 | 53 | /// Borrow the hashmap mutably. 54 | pub fn borrow_mut(&self) -> GcCellRefMut> { 55 | self.0.deref().borrow_mut() 56 | } 57 | 58 | 59 | /// Insert a value in the dict. 60 | pub fn insert(&self, key: Value, value: Value) { 61 | self.borrow_mut().insert(key, value); 62 | } 63 | 64 | 65 | /// Get the value for the given key. 66 | pub fn get(&self, key: &Value) -> Result { 67 | self 68 | .borrow() 69 | .get(key) 70 | .map(Value::copy) 71 | .ok_or(IndexOutOfBounds) 72 | } 73 | 74 | 75 | /// Check if the collections contains the given key 76 | pub fn contains(&self, key: &Value) -> bool { 77 | self 78 | .borrow() 79 | .contains_key(key) 80 | } 81 | 82 | 83 | /// Get the dict length. 84 | pub fn len(&self) -> i64 { 85 | self.borrow().len() as i64 86 | } 87 | 88 | 89 | /// Whether the dict is empty. 90 | pub fn is_empty(&self) -> bool { 91 | self.len() == 0 92 | } 93 | } 94 | 95 | 96 | /// We need PartialOrd in order to be able to store dicts as keys in other dicts. 97 | impl PartialOrd for Dict { 98 | fn partial_cmp(&self, other: &Self) -> Option { 99 | Some(self.cmp(other)) 100 | } 101 | } 102 | 103 | 104 | /// We need Ord in order to be able to store dicts as keys in other dicts. 105 | impl Ord for Dict { 106 | fn cmp(&self, other: &Self) -> Ordering { 107 | // This is very expensive, but there is no better way to correctly compare. 108 | let _self = self.borrow(); 109 | let _self: BTreeMap<&Value, &Value> = _self.iter().collect(); 110 | 111 | let _other = other.borrow(); 112 | let _other: BTreeMap<&Value, &Value> = _other.iter().collect(); 113 | 114 | _self.cmp(&_other) 115 | } 116 | } 117 | 118 | 119 | /// We need Hash in order to be able to store dicts as keys in other dicts. 120 | // GcCell does not implement Eq because `borrow` might panic. 121 | #[allow(clippy::derived_hash_with_manual_eq)] 122 | impl Hash for Dict { 123 | fn hash(&self, state: &mut H) { 124 | // This is very expensive, but there is no better way to correctly compare. 125 | let _self = self.borrow(); 126 | let _self: BTreeMap<&Value, &Value> = _self.iter().collect(); 127 | 128 | _self.hash(state) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/runtime/value/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | hash::{Hash, Hasher}, 3 | io, 4 | ops::Deref, 5 | }; 6 | 7 | use gc::{Gc, GcCell, Finalize, Trace}; 8 | 9 | use super::{IndexOutOfBounds, Value, Str}; 10 | 11 | 12 | /// Strings in Hush are immutable. 13 | #[derive(Debug, Eq, PartialOrd, Ord)] 14 | #[derive(Trace, Finalize)] 15 | pub struct Error { 16 | pub description: Str, 17 | pub context: Gc>, 18 | } 19 | 20 | 21 | impl Error { 22 | /// Create a new error instance. 23 | pub fn new(description: Str, context: Value) -> Self { 24 | Self { 25 | description, 26 | context: Gc::new(GcCell::new(context)), 27 | } 28 | } 29 | 30 | /// Shallow copy. 31 | pub fn copy(&self) -> Self { 32 | Self { 33 | description: self.description.copy(), 34 | context: self.context.clone(), 35 | } 36 | } 37 | 38 | 39 | /// Get the given property. 40 | pub fn get(&self, key: &Value) -> Result { 41 | thread_local! { 42 | pub static DESCRIPTION: Value = "description".into(); 43 | pub static CONTEXT: Value = "context".into(); 44 | } 45 | 46 | match key { 47 | key if DESCRIPTION.with(|desc| key == desc) => Ok( 48 | self.description 49 | .copy() 50 | .into() 51 | ), 52 | 53 | key if CONTEXT.with(|ctx| key == ctx) => Ok( 54 | self.context 55 | .deref() 56 | .borrow() 57 | .copy() 58 | ), 59 | 60 | _ => Err(IndexOutOfBounds) 61 | } 62 | } 63 | } 64 | 65 | 66 | impl PartialEq for Error { 67 | fn eq(&self, other: &Self) -> bool { 68 | self.description == other.description 69 | && *self.context.deref().borrow() == *other.context.deref().borrow() 70 | } 71 | } 72 | 73 | 74 | impl Hash for Error { 75 | fn hash(&self, state: &mut H) { 76 | self.description.hash(state); 77 | self.context.deref().borrow().hash(state); 78 | } 79 | } 80 | 81 | 82 | impl From for Error { 83 | fn from(error: io::Error) -> Self { 84 | Self::new(error.to_string().into(), Value::Nil) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/runtime/value/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | 4 | /// Collection index out of bounds. 5 | #[derive(Debug)] 6 | pub struct IndexOutOfBounds; 7 | 8 | 9 | impl Display for IndexOutOfBounds { 10 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 11 | write!(f, "index out of bounds") 12 | } 13 | } 14 | 15 | 16 | impl std::error::Error for IndexOutOfBounds { } 17 | 18 | 19 | /// Collection is empty. 20 | #[derive(Debug)] 21 | pub struct EmptyCollection; 22 | 23 | 24 | impl Display for EmptyCollection { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | write!(f, "collection is empty") 27 | } 28 | } 29 | 30 | 31 | impl std::error::Error for EmptyCollection { } 32 | -------------------------------------------------------------------------------- /src/runtime/value/float.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | hash::{Hash, Hasher}, 4 | ops::{Add, Sub, Mul, Div, Rem, Neg}, 5 | }; 6 | 7 | use gc::{Finalize, Trace}; 8 | 9 | 10 | /// Hush's float type. 11 | /// This type supports full ordering and hashing. 12 | /// NaN is lower and different than every other value, including itself, but the hash is 13 | /// the same for all NaN values. 14 | #[derive(Debug, Default, Clone)] 15 | #[derive(Trace, Finalize)] 16 | pub struct Float(pub f64); 17 | 18 | 19 | impl Float { 20 | /// Shallow copy. 21 | pub fn copy(&self) -> Self { 22 | Self(self.0) 23 | } 24 | 25 | 26 | /// Check if the float is not a number. 27 | pub fn is_nan(&self) -> bool { 28 | self.0.is_nan() 29 | } 30 | } 31 | 32 | 33 | impl PartialEq for Float { 34 | fn eq(&self, other: &Self) -> bool { 35 | !self.is_nan() 36 | && !other.is_nan() 37 | && self.0 == other.0 38 | } 39 | } 40 | 41 | 42 | impl Eq for Float { } 43 | 44 | 45 | impl PartialOrd for Float { 46 | fn partial_cmp(&self, other: &Self) -> Option { 47 | Some(self.cmp(other)) 48 | } 49 | } 50 | 51 | 52 | impl Ord for Float { 53 | fn cmp(&self, other: &Self) -> Ordering { 54 | match (self.is_nan(), other.is_nan()) { 55 | (true, _) => Ordering::Less, 56 | (false, true) => Ordering::Greater, 57 | (false, false) => self.0 58 | .partial_cmp(&other.0) 59 | .expect("non-nan float comparison failed"), 60 | } 61 | } 62 | } 63 | 64 | 65 | impl Hash for Float { 66 | fn hash(&self, state: &mut H) { 67 | let float = 68 | if self.is_nan() { 69 | f64::NAN // Make sure that the hash equals for all NaN values. 70 | } else { 71 | self.0 72 | }; 73 | 74 | float.to_bits().hash(state) 75 | } 76 | } 77 | 78 | 79 | impl From for Float { 80 | fn from(f: f64) -> Self { 81 | Self(f) 82 | } 83 | } 84 | 85 | 86 | impl From for Float { 87 | fn from(int: i64) -> Self { 88 | Self(int as f64) 89 | } 90 | } 91 | 92 | 93 | impl From<&i64> for Float { 94 | fn from(int: &i64) -> Self { 95 | Self(*int as f64) 96 | } 97 | } 98 | 99 | 100 | impl From<&Float> for i64 { 101 | fn from(float: &Float) -> Self { 102 | float.0 as i64 103 | } 104 | } 105 | 106 | 107 | op_impl!(Float, unary, Neg, neg); 108 | op_impl!(Float, binary, Add, add); 109 | op_impl!(Float, binary, Sub, sub); 110 | op_impl!(Float, binary, Mul, mul); 111 | op_impl!(Float, binary, Div, div); 112 | op_impl!(Float, binary, Rem, rem); 113 | -------------------------------------------------------------------------------- /src/runtime/value/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use crate::{ 4 | fmt::{self, Display}, 5 | symbol, 6 | }; 7 | use super::{Array, Dict, Error, Float, Function, HushFun, RustFun, Str, Value}; 8 | 9 | 10 | impl std::fmt::Display for RustFun { 11 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 12 | write!(f, "{}", self.name()) 13 | } 14 | } 15 | 16 | 17 | impl<'a> Display<'a> for HushFun { 18 | type Context = &'a symbol::Interner; 19 | 20 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 21 | write!(f, "function<{}>", fmt::Show(&self.pos, context)) 22 | } 23 | } 24 | 25 | 26 | impl<'a> Display<'a> for Function { 27 | type Context = &'a symbol::Interner; 28 | 29 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 30 | match self { 31 | Self::Hush(fun) => write!(f, "{}", fmt::Show(fun, context)), 32 | Self::Rust(fun) => write!(f, "{}", fun), 33 | } 34 | } 35 | } 36 | 37 | 38 | impl std::fmt::Display for Float { 39 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 40 | write!(f, "{:#?}", self.0) 41 | } 42 | } 43 | 44 | 45 | impl<'a> Display<'a> for Array { 46 | type Context = &'a symbol::Interner; 47 | 48 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 49 | let array = self.borrow(); 50 | let mut iter = array.iter(); 51 | 52 | write!(f, "[")?; 53 | 54 | if let Some(item) = iter.next() { 55 | write!(f, " {}", fmt::Show(item, context))?; 56 | } 57 | 58 | for item in iter { 59 | write!(f, ", {}", fmt::Show(item, context))?; 60 | } 61 | 62 | write!(f, " ]") 63 | } 64 | } 65 | 66 | 67 | impl<'a> Display<'a> for Dict { 68 | type Context = &'a symbol::Interner; 69 | 70 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 71 | let dict = self.borrow(); 72 | let mut iter = dict.iter(); 73 | 74 | write!(f, "@[")?; 75 | 76 | if let Some((k, v)) = iter.next() { 77 | write!( 78 | f, 79 | " {}: {}", 80 | fmt::Show(k, context), 81 | fmt::Show(v, context) 82 | )?; 83 | } 84 | 85 | for (k, v) in iter { 86 | write!( 87 | f, 88 | ", {}: {}", 89 | fmt::Show(k, context), 90 | fmt::Show(v, context) 91 | )?; 92 | } 93 | 94 | write!(f, " ]") 95 | } 96 | } 97 | 98 | 99 | impl std::fmt::Display for Str { 100 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 101 | write!(f, "\"{}\"", String::from_utf8_lossy(self.as_ref()).escape_debug()) 102 | } 103 | } 104 | 105 | 106 | impl<'a> Display<'a> for Error { 107 | type Context = &'a symbol::Interner; 108 | 109 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 110 | write!( 111 | f, 112 | "error: {} ({})", 113 | self.description, 114 | fmt::Show(self.context.deref().borrow().copy(), context) 115 | ) 116 | } 117 | } 118 | 119 | 120 | impl<'a> Display<'a> for Value { 121 | type Context = &'a symbol::Interner; 122 | 123 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 124 | match self { 125 | Self::Nil => write!(f, "nil"), 126 | Self::Bool(b) => write!(f, "{}", b), 127 | Self::Int(int) => write!(f, "{}", int), 128 | Self::Float(float) => write!(f, "{}", float), 129 | Self::Byte(byte) => write!(f, "{}", *byte as char), 130 | Self::String(string) => write!(f, "{}", string), 131 | Self::Array(array) => write!(f, "{}", fmt::Show(array, context)), 132 | Self::Dict(dict) => write!(f, "{}", fmt::Show(dict, context)), 133 | Self::Function(fun) => write!(f, "{}", fmt::Show(fun, context)), 134 | Self::Error(error) => write!(f, "{}", fmt::Show(error, context)), 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/runtime/value/ops.rs: -------------------------------------------------------------------------------- 1 | /// Implement std::ops for the given type. 2 | macro_rules! op_impl { 3 | ($type: ident, unary, $trait: ident, $method: ident) => { 4 | impl $trait for $type { 5 | type Output = Self; 6 | 7 | fn $method(self) -> Self::Output { 8 | Self(self.0.$method()) 9 | } 10 | } 11 | 12 | impl $trait for &$type { 13 | type Output = $type; 14 | 15 | fn $method(self) -> Self::Output { 16 | $type(self.0.$method()) 17 | } 18 | } 19 | }; 20 | 21 | ($type: ident, binary, $trait: ident, $method: ident) => { 22 | impl $trait for $type { 23 | type Output = Self; 24 | 25 | fn $method(self, rhs: Self) -> Self::Output { 26 | Self(self.0.$method(rhs.0)) 27 | } 28 | } 29 | 30 | impl $trait for &$type { 31 | type Output = $type; 32 | 33 | fn $method(self, rhs: Self) -> Self::Output { 34 | $type(self.0.$method(rhs.0)) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/runtime/value/string.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | ffi::{OsString, OsStr}, 4 | ops::Deref, 5 | os::unix::ffi::{OsStringExt, OsStrExt}, 6 | path::PathBuf, 7 | }; 8 | 9 | use gc::{Gc, Finalize, Trace}; 10 | 11 | use super::{IndexOutOfBounds, Value}; 12 | 13 | 14 | /// Strings in Hush are immutable. 15 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 16 | #[derive(Trace, Finalize)] 17 | pub struct Str(Gc>); 18 | 19 | 20 | impl Str { 21 | /// Shallow copy. 22 | pub fn copy(&self) -> Self { 23 | Self(self.0.clone()) 24 | } 25 | 26 | 27 | /// Get the underlying slice. 28 | pub fn as_bytes(&self) -> &[u8] { 29 | self.as_ref() 30 | } 31 | 32 | 33 | /// Get the value at a given index. 34 | pub fn index(&self, index: i64) -> Result { 35 | let index: usize = index 36 | .try_into() 37 | .map_err(|_| IndexOutOfBounds)?; 38 | 39 | self.0 40 | .get(index) 41 | .copied() 42 | .map(Value::Byte) 43 | .ok_or(IndexOutOfBounds) 44 | } 45 | 46 | 47 | /// Check if the collections contains the given value 48 | pub fn contains(&self, byte: u8) -> bool { 49 | self.0.contains(&byte) 50 | } 51 | 52 | 53 | /// Get the string length. 54 | pub fn len(&self) -> usize { 55 | self.0.len() 56 | } 57 | 58 | 59 | /// Whether the string is empty. 60 | pub fn is_empty(&self) -> bool { 61 | self.len() == 0 62 | } 63 | } 64 | 65 | 66 | impl AsRef<[u8]> for Str { 67 | fn as_ref(&self) -> &[u8] { 68 | self.0.deref().deref() 69 | } 70 | } 71 | 72 | 73 | impl AsRef for Str { 74 | fn as_ref(&self) -> &OsStr { 75 | OsStr::from_bytes(self.as_ref()) 76 | } 77 | } 78 | 79 | 80 | impl<'a> From<&'a [u8]> for Str { 81 | fn from(string: &'a [u8]) -> Self { 82 | Self( 83 | Gc::new(string.into()) 84 | ) 85 | } 86 | } 87 | 88 | 89 | impl From> for Str { 90 | fn from(string: Box<[u8]>) -> Self { 91 | Self( 92 | Gc::new(string) 93 | ) 94 | } 95 | } 96 | 97 | 98 | impl From> for Str { 99 | fn from(vec: Vec) -> Self { 100 | Self::from(vec.into_boxed_slice()) 101 | } 102 | } 103 | 104 | 105 | impl<'a> From<&'a str> for Str { 106 | fn from(string: &'a str) -> Self { 107 | string.as_bytes().into() 108 | } 109 | } 110 | 111 | 112 | impl From> for Str { 113 | fn from(string: Box) -> Self { 114 | let boxed: Box<[u8]> = string.into(); 115 | boxed.into() 116 | } 117 | } 118 | 119 | 120 | impl From for Str { 121 | fn from(string: String) -> Self { 122 | string.into_boxed_str().into() 123 | } 124 | } 125 | 126 | 127 | impl From for Str { 128 | fn from(string: OsString) -> Self { 129 | string.into_vec().into_boxed_slice().into() 130 | } 131 | } 132 | 133 | impl From for Str { 134 | fn from(path: PathBuf) -> Self { 135 | path.into_os_string().into() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/semantic/error/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display as _; 2 | 3 | use super::{Errors, Error, ErrorKind}; 4 | use crate::{ 5 | fmt::{self, Display}, 6 | symbol::{self}, 7 | term::color 8 | }; 9 | 10 | 11 | /// Context for displaying errors. 12 | #[derive(Debug, Copy, Clone)] 13 | pub struct ErrorsDisplayContext<'a> { 14 | /// Max number of displayed errors. 15 | pub max_errors: Option, 16 | /// Symbol interner. 17 | pub interner: &'a symbol::Interner, 18 | } 19 | 20 | 21 | impl<'a> Display<'a> for ErrorKind { 22 | type Context = &'a symbol::Interner; 23 | 24 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 25 | match self { 26 | Self::UndeclaredVariable(symbol) => { 27 | "undeclared variable '".fmt(f)?; 28 | symbol.fmt(f, context)?; 29 | "'".fmt(f) 30 | } 31 | 32 | Self::DuplicateVariable(symbol) => { 33 | "duplicate variable '".fmt(f)?; 34 | symbol.fmt(f, context)?; 35 | "'".fmt(f) 36 | } 37 | 38 | Self::DuplicateKey(symbol) => { 39 | "duplicate key '".fmt(f)?; 40 | symbol.fmt(f, context)?; 41 | "'".fmt(f) 42 | } 43 | 44 | Self::ReturnOutsideFunction => write!(f, "return statement outside function"), 45 | 46 | Self::SelfOutsideFunction => write!(f, "self keyword outside function"), 47 | 48 | Self::TryOutsideFunction => write!(f, "try operator outside function"), 49 | 50 | Self::BreakOutsideLoop => write!(f, "break statement outside loop"), 51 | 52 | Self::InvalidAssignment => write!(f, "invalid assignment"), 53 | 54 | Self::AsyncBuiltin => write!(f, "use of built-in command in async context"), 55 | } 56 | } 57 | } 58 | 59 | 60 | impl<'a> Display<'a> for Error { 61 | type Context = &'a symbol::Interner; 62 | 63 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 64 | write!(f, "{}: {} - ", color::Fg(color::Red, "Error"), fmt::Show(self.pos, context))?; 65 | self.kind.fmt(f, context) 66 | } 67 | } 68 | 69 | 70 | /// We need this in order to be able to implement std::error::Error. 71 | impl std::fmt::Display for Error { 72 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 73 | Display::fmt(self, f, &symbol::Interner::new()) 74 | } 75 | } 76 | 77 | 78 | impl<'a> Display<'a> for Errors { 79 | type Context = ErrorsDisplayContext<'a>; 80 | 81 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 82 | for (ix, error) in self.0.iter().enumerate() { 83 | if let Some(max) = context.max_errors { 84 | if max <= ix { 85 | writeln!( 86 | f, 87 | "{} {}", 88 | color::Fg(color::Red, max), 89 | color::Fg(color::Red, "more supressed semantic errors"), 90 | )?; 91 | 92 | break; 93 | } 94 | } 95 | 96 | writeln!(f, "{}", fmt::Show(error, context.interner))?; 97 | } 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | 104 | /// We need this in order to be able to implement std::error::Error. 105 | impl std::fmt::Display for Errors { 106 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 107 | for error in self.0.iter() { 108 | Display::fmt(error, f, &symbol::Interner::new())?; 109 | } 110 | 111 | Ok(()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/semantic/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use super::{Symbol, SourcePos}; 4 | pub use fmt::ErrorsDisplayContext; 5 | 6 | 7 | /// The kind of a semantic error. 8 | #[derive(Debug)] 9 | pub enum ErrorKind { 10 | /// Variable usage before variable declaration. 11 | UndeclaredVariable(Symbol), 12 | /// Variable declared with the same name twice in the same scope. 13 | /// Includes function parameters. 14 | DuplicateVariable(Symbol), 15 | /// Duplicate keys in dict literal. 16 | DuplicateKey(Symbol), 17 | /// Return statement outside function. 18 | ReturnOutsideFunction, 19 | /// Self keyword outside function. 20 | SelfOutsideFunction, 21 | /// Try operator outside function. 22 | TryOutsideFunction, 23 | /// Break statement outside loop. 24 | BreakOutsideLoop, 25 | /// Invalid assignment l-value. 26 | InvalidAssignment, 27 | /// Built-in command used in async context. 28 | /// Async contexts include pipes, redirections and capture or async blocks. 29 | AsyncBuiltin, 30 | } 31 | 32 | 33 | /// A semantic error. 34 | #[derive(Debug)] 35 | pub struct Error { 36 | pub kind: ErrorKind, 37 | pub pos: SourcePos, 38 | } 39 | 40 | 41 | impl Error { 42 | /// Variable usage before variable declaration. 43 | pub fn undeclared_variable(symbol: Symbol, pos: SourcePos) -> Self { 44 | Self { 45 | kind: ErrorKind::UndeclaredVariable(symbol), 46 | pos 47 | } 48 | } 49 | 50 | 51 | /// Variable declared with the same name twice in the same scope. 52 | /// Includes function parameters. 53 | pub fn duplicate_variable(symbol: Symbol, pos: SourcePos) -> Self { 54 | Self { 55 | kind: ErrorKind::DuplicateVariable(symbol), 56 | pos 57 | } 58 | } 59 | 60 | 61 | /// Duplicate keys in dict literal. 62 | pub fn duplicate_key(symbol: Symbol, pos: SourcePos) -> Self { 63 | Self { 64 | kind: ErrorKind::DuplicateKey(symbol), 65 | pos 66 | } 67 | } 68 | 69 | 70 | /// Return statement outside function. 71 | pub fn return_outside_function(pos: SourcePos) -> Self { 72 | Self { 73 | kind: ErrorKind::ReturnOutsideFunction, 74 | pos 75 | } 76 | } 77 | 78 | 79 | /// Self keyword outside function. 80 | pub fn self_outside_function(pos: SourcePos) -> Self { 81 | Self { 82 | kind: ErrorKind::SelfOutsideFunction, 83 | pos 84 | } 85 | } 86 | 87 | 88 | /// Try operator outside function. 89 | pub fn try_outside_function(pos: SourcePos) -> Self { 90 | Self { 91 | kind: ErrorKind::TryOutsideFunction, 92 | pos 93 | } 94 | } 95 | 96 | 97 | /// Break statement outside loop. 98 | pub fn break_outside_loop(pos: SourcePos) -> Self { 99 | Self { 100 | kind: ErrorKind::BreakOutsideLoop, 101 | pos 102 | } 103 | } 104 | 105 | 106 | /// Invalid assignment l-value. 107 | pub fn invalid_assignment(pos: SourcePos) -> Self { 108 | Self { 109 | kind: ErrorKind::InvalidAssignment, 110 | pos 111 | } 112 | } 113 | 114 | 115 | /// Built-in command used in async context. 116 | /// Async contexts include pipes redirections and capture or async blocks. 117 | pub fn async_builtin(pos: SourcePos) -> Self { 118 | Self { 119 | kind: ErrorKind::AsyncBuiltin, 120 | pos 121 | } 122 | } 123 | } 124 | 125 | 126 | /// A collection of semantic errors. 127 | #[derive(Debug, Default)] 128 | pub struct Errors(pub Vec); 129 | 130 | 131 | impl IntoIterator for Errors { 132 | type Item = Error; 133 | type IntoIter = std::vec::IntoIter; 134 | 135 | fn into_iter(self) -> Self::IntoIter { 136 | self.0.into_iter() 137 | } 138 | } 139 | 140 | 141 | impl Extend for Errors { 142 | fn extend(&mut self, iter: T) 143 | where 144 | T : IntoIterator, 145 | { 146 | self.0.extend(iter) 147 | } 148 | } 149 | 150 | 151 | impl std::error::Error for Errors { } 152 | -------------------------------------------------------------------------------- /src/semantic/program/command.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::io::FileDescriptor; 4 | use super::{ast, mem, SourcePos}; 5 | 6 | 7 | /// The most basic part of an argument. 8 | #[derive(Debug)] 9 | pub enum ArgUnit { 10 | Literal(Box<[u8]>), 11 | Dollar { 12 | slot_ix: mem::SlotIx, 13 | pos: SourcePos, 14 | } 15 | } 16 | 17 | 18 | /// The most basic part of an argument. 19 | #[derive(Debug)] 20 | pub enum ArgPart { 21 | Unit(ArgUnit), 22 | 23 | // Literal expansions: 24 | Home, // ~/ 25 | Range(i64, i64), // {x..y} 26 | Collection(Box<[ArgUnit]>), // {a,b,c} 27 | 28 | // File expansions: 29 | Star, // * 30 | Percent, // % 31 | CharClass(Box<[u8]>), // [...] 32 | } 33 | 34 | 35 | /// An argument may consist of several argument parts. 36 | #[derive(Debug)] 37 | pub struct Argument { 38 | pub parts: Box<[ArgPart]>, 39 | pub pos: SourcePos, 40 | } 41 | 42 | 43 | /// The target of a redirection operation. 44 | #[derive(Debug)] 45 | pub enum RedirectionTarget { 46 | /// Redirect to a file descriptor. 47 | Fd(FileDescriptor), 48 | /// Overwrite a file. 49 | Overwrite(Argument), 50 | /// Append to a file. 51 | Append(Argument), 52 | } 53 | 54 | 55 | /// Redirection operation. 56 | #[derive(Debug)] 57 | pub enum Redirection { 58 | /// Redirect output to a file or file descriptor. 59 | Output { 60 | source: FileDescriptor, 61 | target: RedirectionTarget, 62 | }, 63 | /// Redirect input from a file or literal. 64 | Input { 65 | /// Whether the source is the input or the file path. 66 | literal: bool, 67 | source: Argument, 68 | }, 69 | } 70 | 71 | 72 | /// Built-in commands. 73 | #[derive(Debug, Copy, Clone)] 74 | pub enum Builtin { 75 | Alias, 76 | Cd, 77 | Exec, 78 | Exec0, 79 | Spawn0, 80 | } 81 | 82 | 83 | #[derive(Debug)] 84 | pub struct InvalidBuiltin; 85 | 86 | 87 | impl std::error::Error for InvalidBuiltin { } 88 | 89 | 90 | impl<'a> TryFrom<&'a [u8]> for Builtin { 91 | type Error = InvalidBuiltin; 92 | 93 | fn try_from(value: &'a [u8]) -> Result { 94 | match value { 95 | b"alias" => Ok(Self::Alias), 96 | b"cd" => Ok(Self::Cd), 97 | b"exec" => Ok(Self::Exec), 98 | b"exec0" => Ok(Self::Exec0), 99 | b"spawn0" => Ok(Self::Spawn0), 100 | _ => Err(InvalidBuiltin) 101 | } 102 | } 103 | } 104 | 105 | 106 | impl<'a> TryFrom<&'a ast::Argument> for Builtin { 107 | type Error = InvalidBuiltin; 108 | 109 | fn try_from(arg: &'a ast::Argument) -> Result { 110 | match arg.parts.as_ref() { 111 | [ ast::ArgPart::Unit(ast::ArgUnit::Literal(ref lit)) ] => Self::try_from(lit.as_ref()), 112 | _ => Err(InvalidBuiltin), 113 | } 114 | } 115 | } 116 | 117 | 118 | /// A single command, including possible redirections and try operator. 119 | #[derive(Debug)] 120 | pub struct BasicCommand { 121 | pub program: Argument, 122 | /// Key-value pairs of environment variables. 123 | pub env: Box<[(ArgUnit, Argument)]>, 124 | pub arguments: Box<[Argument]>, 125 | pub redirections: Box<[Redirection]>, 126 | pub abort_on_error: bool, 127 | pub pos: SourcePos, 128 | } 129 | 130 | 131 | /// Commands may be pipelines, or a single BasicCommand. 132 | #[derive(Debug)] 133 | pub enum Command { 134 | Builtin { 135 | program: Builtin, 136 | arguments: Box<[Argument]>, 137 | abort_on_error: bool, 138 | pos: SourcePos, 139 | }, 140 | External { 141 | head: BasicCommand, 142 | tail: Box<[BasicCommand]> 143 | } 144 | } 145 | 146 | 147 | /// A command block. 148 | #[derive(Debug)] 149 | pub struct CommandBlock { 150 | pub kind: CommandBlockKind, 151 | pub head: Command, 152 | pub tail: Box<[Command]>, 153 | } 154 | 155 | 156 | /// The kinds of command blocks. 157 | #[derive(Debug)] 158 | pub enum CommandBlockKind { 159 | Synchronous, // {} 160 | Asynchronous, // &{} 161 | Capture, // ${} 162 | } 163 | 164 | 165 | impl From for CommandBlockKind { 166 | fn from(kind: ast::CommandBlockKind) -> Self { 167 | match kind { 168 | ast::CommandBlockKind::Synchronous => CommandBlockKind::Synchronous, 169 | ast::CommandBlockKind::Asynchronous => CommandBlockKind::Asynchronous, 170 | ast::CommandBlockKind::Capture => CommandBlockKind::Capture, 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/semantic/program/mem/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display as _; 2 | 3 | use super::{lexer, Capture, FrameInfo, SlotIx}; 4 | use crate::{ 5 | fmt::{self, Display, Indentation}, 6 | term::color 7 | }; 8 | 9 | 10 | impl std::fmt::Display for SlotIx { 11 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 12 | color::Fg(color::Green, '#').fmt(f)?; 13 | color::Fg(color::Green, self.0).fmt(f) 14 | } 15 | } 16 | 17 | 18 | impl<'a> Display<'a> for FrameInfo { 19 | type Context = Option; 20 | 21 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 22 | #[derive(Clone)] 23 | enum Slot { 24 | Regular, 25 | Capture { from: SlotIx }, 26 | } 27 | 28 | let mut slots: Box<[(SlotIx, Slot)]> = std::iter 29 | ::repeat(Slot::Regular) 30 | .take(self.slots.0 as usize) 31 | .enumerate() 32 | .map( 33 | |(ix, slot)| (SlotIx(ix as u32), slot) 34 | ) 35 | .collect(); 36 | 37 | for Capture { from, to } in self.captures.iter().copied() { 38 | slots[to.0 as usize].1 = Slot::Capture { from }; 39 | } 40 | 41 | fmt::sep_by( 42 | slots.iter(), 43 | f, 44 | |(slot_ix, slot), f| { 45 | if let Some(indent) = context { 46 | indent.fmt(f)?; 47 | } else { 48 | " ".fmt(f)?; 49 | } 50 | 51 | lexer::Keyword::Let.fmt(f)?; 52 | " ".fmt(f)?; 53 | slot_ix.fmt(f)?; 54 | ": ".fmt(f)?; 55 | 56 | match slot { 57 | Slot::Regular => color::Fg(color::Blue, "auto").fmt(f)?, 58 | Slot::Capture { from } => { 59 | color::Fg(color::Blue, "capture").fmt(f)?; 60 | " ".fmt(f)?; 61 | from.fmt(f)?; 62 | } 63 | } 64 | 65 | if Some(*slot_ix) == self.self_slot { 66 | color::Fg(color::Blue, " self").fmt(f)?; 67 | } 68 | 69 | Ok(()) 70 | }, 71 | if context.is_some() { "\n" } else { ";" }, 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/semantic/program/mem/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use super::lexer; 4 | 5 | 6 | /// The index of a memory slot in the activation record. 7 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 8 | pub struct SlotIx(pub u32); 9 | 10 | 11 | impl SlotIx { 12 | pub(in crate::semantic) fn bump(&mut self) -> SlotIx { 13 | let previous = *self; 14 | self.0 += 1; 15 | previous 16 | } 17 | } 18 | 19 | 20 | /// How to capture a value from the parent scope. 21 | #[derive(Debug, Copy, Clone)] 22 | pub struct Capture { 23 | /// The slot index in the parent scope. 24 | pub from: SlotIx, 25 | /// The slot index in our scope. 26 | pub to: SlotIx, 27 | } 28 | 29 | 30 | /// The mold for a stack frame. 31 | #[derive(Debug)] 32 | pub struct FrameInfo { 33 | /// How many slots in the activation record. 34 | pub slots: SlotIx, 35 | /// Captured values from parent scope. 36 | pub captures: Box<[Capture]>, 37 | /// Where to insert `self`. 38 | pub self_slot: Option, 39 | } 40 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/async-builtin-1.hsh: -------------------------------------------------------------------------------- 1 | let y = &{ alias } 2 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/async-builtin-2.hsh: -------------------------------------------------------------------------------- 1 | { foo | bar | cd | baz }.status 2 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/async-builtin-3.hsh: -------------------------------------------------------------------------------- 1 | { 2 | hello > file; 3 | cd > "well you can't do that" 4 | } 5 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/break-outside-loop-1.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | break 3 | end 4 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/break-outside-loop-2.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | while true do 3 | function test() 4 | break 5 | end 6 | 7 | test() 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/duplicate-dict-keys-1.hsh: -------------------------------------------------------------------------------- 1 | @[ 2 | foo: 1, 3 | bar: 2, 4 | foo: 3, 5 | ] 6 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/invalid-assignment-1.hsh: -------------------------------------------------------------------------------- 1 | function hello() 2 | let x 3 | foo().bar.baz[2].boo() = 2 4 | end 5 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/invalid-assignment-2.hsh: -------------------------------------------------------------------------------- 1 | function hello() 2 | self = 2 3 | end 4 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/return-outside-function-1.hsh: -------------------------------------------------------------------------------- 1 | let condition 2 | 3 | if condition then 4 | return 5 | end 6 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/try-outside-function.hsh: -------------------------------------------------------------------------------- 1 | for result in std.iter([]) do 2 | result? 3 | end 4 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/undeclared-variable-1.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | x = 2 3 | let x = 1 4 | end 5 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/undeclared-variable-2.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | if true then 3 | let x = 1 4 | x 5 | else 6 | x 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/semantic/tests/data/negative/undeclared-variable-3.hsh: -------------------------------------------------------------------------------- 1 | { echo $hello } 2 | -------------------------------------------------------------------------------- /src/semantic/tests/data/positive/closures.hsh: -------------------------------------------------------------------------------- 1 | function fun() 2 | let x = 0 3 | 4 | let fun = function() 5 | let fun = function() 6 | return x 7 | end 8 | 9 | x = 2 10 | return fun 11 | end 12 | 13 | x = 1 14 | return fun 15 | end 16 | 17 | fun()()() # returns 2 18 | 19 | 20 | fun = function () 21 | let x = "Hello world!" 22 | 23 | return @[ 24 | foo: function () 25 | x = "Foo" 26 | end, 27 | 28 | bar: function () 29 | x = "Bar" 30 | end, 31 | 32 | print: function () 33 | std.println(x) 34 | end, 35 | ] 36 | end 37 | 38 | 39 | let obj = fun() 40 | 41 | obj.print() # Hello world! 42 | obj.foo() 43 | obj.print() # Foo 44 | obj.bar() 45 | obj.print() # Bar 46 | 47 | 48 | fun = function () 49 | let x = 1 50 | 51 | function foo() 52 | # Both functions below capture x, which implies that foo captures x too. 53 | @[ 54 | bar: function () 55 | x 56 | end, 57 | baz: function() 58 | x 59 | end 60 | ] 61 | end 62 | 63 | foo 64 | end 65 | 66 | fun()().bar() # 1 67 | -------------------------------------------------------------------------------- /src/semantic/tests/data/positive/scope.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | if true then 3 | let x = 2 4 | std.println(x) 5 | else 6 | let x = 1 7 | std.println(x) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/semantic/tests/data/positive/try.hsh: -------------------------------------------------------------------------------- 1 | function oops(ill) 2 | if ill then 3 | std.error("something went wrong", nil) 4 | else 5 | "it works!" 6 | end 7 | end 8 | 9 | 10 | function handle() 11 | let first = oops(false)? 12 | std.println(first) 13 | 14 | let second = oops(true)? 15 | std.println(second) 16 | end 17 | -------------------------------------------------------------------------------- /src/semantic/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | path::Path, 4 | os::unix::ffi::OsStrExt, 5 | }; 6 | 7 | use crate::{fmt, semantic::ErrorsDisplayContext, symbol, syntax::{self, AnalysisDisplayContext}, tests}; 8 | use super::{program, Analyzer, Program, Errors}; 9 | 10 | 11 | fn test_dir(path: P, mut check: F) -> io::Result<()> 12 | where 13 | P: AsRef, 14 | F: FnMut(&Result) -> bool, 15 | { 16 | let mut interner = symbol::Interner::new(); 17 | 18 | tests::util::test_dir( 19 | path, 20 | move |path, file| { 21 | let path_symbol = interner.get_or_intern(path.as_os_str().as_bytes()); 22 | let source = syntax::Source::from_reader(path_symbol, file)?; 23 | let syntactic_analysis = syntax::Analysis::analyze(&source, &mut interner); 24 | 25 | if !syntactic_analysis.errors.is_empty() { 26 | panic!( 27 | "{}", 28 | fmt::Show( 29 | syntactic_analysis, 30 | AnalysisDisplayContext { 31 | max_errors: None, 32 | interner: &interner, 33 | } 34 | ) 35 | ); 36 | } 37 | 38 | let result = Analyzer::analyze(syntactic_analysis.ast, &mut interner); 39 | 40 | if !check(&result) { 41 | match result { 42 | Ok(program) => panic!( 43 | "{}", 44 | fmt::Show( 45 | program, 46 | program::fmt::Context::from(&interner), 47 | ) 48 | ), 49 | 50 | Err(errors) => panic!( 51 | "{}", 52 | fmt::Show( 53 | errors, 54 | ErrorsDisplayContext { 55 | max_errors: None, 56 | interner: &interner, 57 | } 58 | ) 59 | ), 60 | } 61 | } 62 | 63 | Ok(()) 64 | } 65 | ) 66 | } 67 | 68 | 69 | #[test] 70 | fn test_examples() -> io::Result<()> { 71 | test_dir( 72 | "examples/hush", 73 | Result::is_ok, 74 | ) 75 | } 76 | 77 | 78 | #[test] 79 | fn test_positive() -> io::Result<()> { 80 | test_dir( 81 | "src/semantic/tests/data/positive", 82 | Result::is_ok 83 | ) 84 | } 85 | 86 | 87 | #[test] 88 | fn test_negative() -> io::Result<()> { 89 | test_dir( 90 | "src/semantic/tests/data/negative", 91 | Result::is_err, 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/symbol/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt::Display as _}; 2 | 3 | use super::{Interner, Symbol}; 4 | use crate::{ 5 | syntax::ast::fmt::ILL_FORMED, 6 | fmt::Display, 7 | term::color, 8 | }; 9 | 10 | 11 | impl<'a> Display<'a> for Symbol { 12 | type Context = &'a Interner; 13 | 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>, context: Self::Context) -> std::fmt::Result { 15 | if *self == Self::default() { 16 | ILL_FORMED.fmt(f) 17 | } else { 18 | let ident: Cow<[u8]> = match context.resolve(*self) { 19 | Some(id) => id.into(), 20 | None => format!("", Into::::into(*self)).into_bytes().into(), 21 | }; 22 | 23 | let ident = String::from_utf8_lossy(&ident); 24 | let ident = ident.escape_debug(); 25 | 26 | color::Fg(color::Green, ident).fmt(f) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/symbol/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use intaglio::{Symbol as SymbolInner, bytes::SymbolTable}; 4 | 5 | 6 | /// A symbol is a reference to an value stored in the symbol interner. 7 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 8 | pub struct Symbol(SymbolInner); 9 | 10 | 11 | /// The default symbol is a dummy symbol, which will yield "" when 12 | /// resolved. 13 | impl Default for Symbol { 14 | fn default() -> Self { 15 | Self(SymbolInner::new(0)) 16 | } 17 | } 18 | 19 | 20 | impl From for usize { 21 | fn from(symbol: Symbol) -> usize { 22 | symbol.0.id() as usize 23 | } 24 | } 25 | 26 | 27 | /// A symbol interner, used to store identifiers, paths, etc. 28 | #[derive(Debug)] 29 | pub struct Interner(SymbolTable); 30 | 31 | 32 | impl Interner { 33 | /// Create a new interner. Please note that this allocates memory even if no symbols are 34 | /// inserted. 35 | pub fn new() -> Self { 36 | let mut interner = SymbolTable::new(); 37 | interner 38 | .intern(b"".as_ref()) 39 | .expect("failed to intern symbol"); 40 | Self(interner) 41 | } 42 | 43 | 44 | /// Get the symbol for a value. 45 | #[cfg(test)] 46 | pub fn get(&self, value: T) -> Option 47 | where 48 | T: AsRef<[u8]>, 49 | { 50 | self.0 51 | .check_interned(value.as_ref()) 52 | .map(Symbol) 53 | } 54 | 55 | 56 | /// Get the symbol for a value. The value is interned if needed. 57 | pub fn get_or_intern(&mut self, value: T) -> Symbol 58 | where 59 | T: AsRef<[u8]>, 60 | { 61 | let value = value.as_ref().to_owned(); 62 | 63 | Symbol( 64 | self.0 65 | .intern(value) 66 | .expect("failed to intern symbol") 67 | ) 68 | } 69 | 70 | 71 | /// Resolve the string for a symbol. 72 | pub fn resolve(&self, symbol: Symbol) -> Option<&[u8]> { 73 | self.0.get(symbol.0) 74 | } 75 | 76 | 77 | /// Get the number of interned strings. 78 | /// This does not include the dummy symbol. 79 | #[cfg(test)] 80 | pub fn len(&self) -> usize { 81 | self.0.len() - 1 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/syntax/error/fmt.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, Errors, AnalysisDisplayContext}; 2 | use crate::{ 3 | fmt::{self, Display}, 4 | symbol, 5 | term::color 6 | }; 7 | 8 | 9 | impl<'a> Display<'a> for Error { 10 | type Context = &'a symbol::Interner; 11 | 12 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 13 | match self { 14 | Self::Lexer(error) => error.fmt(f, context), 15 | Self::Parser(error) => error.fmt(f, context), 16 | } 17 | } 18 | } 19 | 20 | 21 | /// We need this in order to be able to implement std::error::Error. 22 | impl std::fmt::Display for Error { 23 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 24 | Display::fmt(self, f, &symbol::Interner::new()) 25 | } 26 | } 27 | 28 | 29 | impl<'a> Display<'a> for Errors { 30 | type Context = AnalysisDisplayContext<'a>; 31 | 32 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 33 | for (ix, error) in self.0.iter().enumerate() { 34 | if let Some(max) = context.max_errors { 35 | if max <= ix { 36 | writeln!( 37 | f, 38 | "{} {}", 39 | color::Fg(color::Red, max), 40 | color::Fg(color::Red, "more supressed syntax errors"), 41 | )?; 42 | 43 | break; 44 | } 45 | } 46 | 47 | writeln!( 48 | f, 49 | "{}: {}", 50 | color::Fg(color::Red, "Error"), 51 | fmt::Show(error, context.interner) 52 | )?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/syntax/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use super::{lexer, parser, AnalysisDisplayContext}; 4 | 5 | 6 | /// Syntax error. 7 | #[derive(Debug)] 8 | pub enum Error { 9 | Lexer(lexer::Error), 10 | Parser(parser::Error), 11 | } 12 | 13 | 14 | impl std::error::Error for Error {} 15 | 16 | 17 | /// Syntax errors. 18 | #[derive(Debug)] 19 | pub struct Errors(pub Box<[Error]>); 20 | 21 | 22 | impl Errors { 23 | /// Check if there are any errors. 24 | pub fn is_empty(&self) -> bool { 25 | self.0.is_empty() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/syntax/fmt.rs: -------------------------------------------------------------------------------- 1 | use super::{ast, Analysis}; 2 | use crate::{ 3 | fmt::Display, 4 | symbol, 5 | }; 6 | 7 | 8 | /// Context for displaying the syntax analysis. 9 | #[derive(Debug, Copy, Clone)] 10 | pub struct AnalysisDisplayContext<'a> { 11 | /// Max number of displayed errors. 12 | pub max_errors: Option, 13 | /// Symbol interner. 14 | pub interner: &'a symbol::Interner, 15 | } 16 | 17 | 18 | impl<'a> Display<'a> for Analysis { 19 | type Context = AnalysisDisplayContext<'a>; 20 | 21 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 22 | self.errors.fmt(f, context)?; 23 | self.ast.fmt(f, ast::fmt::Context::from(context.interner)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/command.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | symbol::CommandSymbolChar, 3 | Argument, 4 | CommandSymbol, 5 | Comment, 6 | Cursor, 7 | Error, 8 | Root, 9 | State, 10 | Token, 11 | TokenKind, 12 | Transition, 13 | }; 14 | 15 | 16 | /// The state for lexing command blocks. 17 | #[derive(Debug)] 18 | pub(super) struct Command; 19 | 20 | 21 | impl Command { 22 | pub fn visit(self, cursor: &Cursor) -> Transition { 23 | match cursor.peek() { 24 | // Whitespace. 25 | Some(c) if c.is_ascii_whitespace() => Transition::step(self), 26 | 27 | // Comment. 28 | Some(b'#') => Transition::step(Comment::from(self)), 29 | 30 | // Close command block. 31 | Some(b'}') => Transition::produce( 32 | Root, 33 | Token { kind: TokenKind::CloseCommand, pos: cursor.pos() }, 34 | ), 35 | 36 | // Argument or operator. 37 | Some(c) => match CommandSymbolChar::from_first(c) { 38 | // Argument. 39 | CommandSymbolChar::None => Transition::resume(Argument::at(cursor)), 40 | 41 | // Semicolon, pipe or try. 42 | CommandSymbolChar::Single(token) => { 43 | Transition::produce(self, Token { kind: token, pos: cursor.pos() }) 44 | } 45 | 46 | // >, >>, <, <<. 47 | CommandSymbolChar::Double { first } => { 48 | Transition::step(CommandSymbol::from_first(first, cursor)) 49 | } 50 | }, 51 | 52 | // Eof. 53 | None => Transition::error(Root, Error::unexpected_eof(cursor.pos())), 54 | } 55 | } 56 | } 57 | 58 | 59 | impl From for State { 60 | fn from(state: Command) -> State { 61 | Self::Command(state) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/comment.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, Cursor, Root, State, Transition}; 2 | 3 | /// The state for lexing comments. 4 | /// This state is generic in the sense that it returns to the previous state once the 5 | /// entire comment is consumed. 6 | #[derive(Debug)] 7 | pub(super) struct Comment(S); 8 | 9 | 10 | impl Comment 11 | where 12 | S: Into, 13 | State: From, 14 | { 15 | pub fn visit(self, cursor: &Cursor) -> Transition { 16 | match cursor.peek() { 17 | // Newline marks the end of the comment. 18 | Some(b'\n') => Transition::resume(self.0), 19 | 20 | // Otherwise, eat everything. 21 | _ => Transition::step(self), 22 | } 23 | } 24 | } 25 | 26 | 27 | impl From for Comment { 28 | fn from(state: S) -> Self { 29 | Self(state) 30 | } 31 | } 32 | 33 | 34 | impl From> for State { 35 | fn from(state: Comment) -> State { 36 | Self::Comment(state) 37 | } 38 | } 39 | 40 | 41 | impl From> for State { 42 | fn from(state: Comment) -> State { 43 | Self::CommandComment(state) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/expansion.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | argument, 3 | Argument, 4 | ArgExpansion, 5 | Cursor, 6 | Checkpoint, 7 | State, 8 | Transition, 9 | }; 10 | 11 | 12 | /// The state context for the Expansion state. 13 | /// The Expansion state is generic in the sense that it returns to the previous state once 14 | /// it is finished. Such previous state is the ExpansionContext. Unless an expansion is 15 | /// successfully produced, the cursor will be reset to where it when the expansion context 16 | /// started. 17 | pub(super) trait ExpansionContext { 18 | /// The transition to make when a expansion has been produced. 19 | fn produce(self, expansion: ArgExpansion) -> Transition; 20 | /// The transition to make when no expansion could be parsed. 21 | /// Yield and rollback to the given checkpoint. 22 | fn rollback(self, checkpoint: Checkpoint) -> Transition; 23 | /// Check if a character may be consumed inside expansions. 24 | fn is_expansion_word(value: u8) -> bool; 25 | } 26 | 27 | 28 | /// The base state for lexing expansions. 29 | #[derive(Debug)] 30 | pub(super) struct Expansion { 31 | start: Checkpoint, 32 | /// Whether to allow recognition of the home expansion. 33 | allow_home: bool, 34 | /// Whether the tilde has been consumed for the home expansion. 35 | tilde_consumed: bool, 36 | /// The argument context. 37 | context: C, 38 | } 39 | 40 | 41 | impl Expansion 42 | where 43 | C: ExpansionContext, 44 | State: From, 45 | { 46 | pub fn at(cursor: &Cursor, allow_home: bool, context: C) -> Self { 47 | Self { 48 | start: cursor.checkpoint(), 49 | allow_home, 50 | tilde_consumed: false, 51 | context, 52 | } 53 | } 54 | 55 | 56 | pub fn visit(mut self, cursor: &Cursor) -> Transition { 57 | // Note that we must only allow home expansion in the beggining of the input. 58 | let allow_home = self.allow_home; 59 | self.allow_home = false; 60 | 61 | match cursor.peek() { 62 | // Home expansion start. 63 | Some(b'~') if allow_home => { 64 | self.tilde_consumed = true; 65 | Transition::step(self) 66 | } 67 | 68 | // Home expansion end. 69 | Some(b'/') if self.tilde_consumed => { 70 | self.context.produce(ArgExpansion::Home) 71 | } 72 | 73 | // Home expansion missing tilde. 74 | Some(_) if self.tilde_consumed => self.context.rollback(self.start), 75 | 76 | // Star. 77 | Some(b'*') => { 78 | self.context.produce(ArgExpansion::Star) 79 | } 80 | 81 | // Percent. 82 | Some(b'%') => { 83 | self.context.produce(ArgExpansion::Percent) 84 | } 85 | 86 | Some(b'[') => { 87 | todo!() // char class. 88 | } 89 | 90 | Some(b'{') => { 91 | todo!() // range, collection 92 | } 93 | 94 | // Failed to parse expansion. 95 | _ => self.context.rollback(self.start) 96 | } 97 | } 98 | } 99 | 100 | 101 | impl From> for State { 102 | fn from(state: Expansion) -> Self { 103 | Self::Expansion(state) 104 | } 105 | } 106 | 107 | 108 | impl From>> for State { 109 | fn from(state: Expansion>) -> Self { 110 | Self::ExpansionWord(state) 111 | } 112 | } 113 | 114 | 115 | /// Whether a character is an expansion starter. 116 | pub fn is_start(c: u8) -> bool { 117 | b"{[~*%".contains(&c) 118 | } 119 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/number.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | Cursor, 3 | Error, 4 | ErrorKind, 5 | Literal, 6 | Root, 7 | SourcePos, 8 | State, 9 | Token, 10 | TokenKind, 11 | Transition, 12 | }; 13 | 14 | 15 | /// The state for lexing numeric literals, both integer and float. 16 | #[derive(Debug)] 17 | pub(super) struct NumberLiteral { 18 | start_offset: usize, 19 | consumed_decimal: Option, 20 | consumed_exponent: Option, 21 | pos: SourcePos, 22 | } 23 | 24 | 25 | impl NumberLiteral { 26 | pub fn at(cursor: &Cursor) -> Self { 27 | Self { 28 | start_offset: cursor.offset(), 29 | consumed_decimal: None, 30 | consumed_exponent: None, 31 | pos: cursor.pos(), 32 | } 33 | } 34 | 35 | 36 | pub fn visit(mut self, cursor: &Cursor) -> Transition { 37 | let error = |error| Transition::error(Root, Error { error, pos: self.pos }); 38 | 39 | match (&self, cursor.peek()) { 40 | // There must be up to one dot, and it must precede the exponent. 41 | ( 42 | &Self { 43 | consumed_decimal: None, consumed_exponent: None, .. 44 | }, 45 | Some(b'.'), 46 | ) => { 47 | self.consumed_decimal = Some(false); 48 | Transition::step(self) 49 | } 50 | 51 | // Exponent may be present regardless of dot. 52 | (&Self { consumed_exponent: None, .. }, Some(c)) if c == b'e' || c == b'E' => { 53 | self.consumed_exponent = Some(false); 54 | Transition::step(self) 55 | } 56 | 57 | // Consume digits. 58 | (_, Some(value)) if value.is_ascii_digit() => { 59 | // If a dot or an exponent preceded, then set the according flag. 60 | if self.consumed_decimal == Some(false) { 61 | self.consumed_decimal = Some(true); 62 | } 63 | if self.consumed_exponent == Some(false) { 64 | self.consumed_exponent = Some(true); 65 | } 66 | 67 | Transition::step(self) 68 | } 69 | 70 | // A dot or an exponent must be followed by a digit. 71 | (&Self { consumed_decimal: Some(false), .. }, value) 72 | | (&Self { consumed_exponent: Some(false), .. }, value) => { 73 | if let Some(value) = value { 74 | error(ErrorKind::Unexpected(value)) 75 | } else { 76 | error(ErrorKind::UnexpectedEof) 77 | } 78 | } 79 | 80 | // Stop and produce if a non-digit is found, including EOF. 81 | (_, _) => match self.parse(cursor) { 82 | Ok(token) => Transition::resume_produce(Root, token), 83 | Err(error) => Transition::error(Root, error), 84 | }, 85 | } 86 | } 87 | 88 | 89 | /// Parse the consumed characters. 90 | fn parse(&self, cursor: &Cursor) -> Result { 91 | let number = &cursor.slice()[self.start_offset .. cursor.offset()]; 92 | 93 | let literal = |literal| Ok(Token { kind: TokenKind::Literal(literal), pos: self.pos }); 94 | 95 | // There is no method in std to parse a number from a byte array. 96 | let number_str = std::str::from_utf8(number) 97 | .expect("number literals should be valid ascii, which should be valid utf8"); 98 | 99 | if self.is_float() { 100 | match number_str.parse() { 101 | Ok(float) => literal(Literal::Float(float)), 102 | Err(_) => Err(Error::invalid_number(number, self.pos)), 103 | } 104 | } else { 105 | match number_str.parse() { 106 | Ok(int) => literal(Literal::Int(int)), 107 | Err(_) => Err(Error::invalid_number(number, self.pos)), 108 | } 109 | } 110 | } 111 | 112 | 113 | /// Check if the consumed characters constitue a float. 114 | fn is_float(&self) -> bool { 115 | self.consumed_decimal.is_some() || self.consumed_exponent.is_some() 116 | } 117 | } 118 | 119 | 120 | impl From for State { 121 | fn from(state: NumberLiteral) -> State { 122 | Self::NumberLiteral(state) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/root.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | symbol::SymbolChar, 3 | word::IsWord, 4 | ByteLiteral, 5 | Command, 6 | Comment, 7 | Cursor, 8 | Error, 9 | NumberLiteral, 10 | State, 11 | StringLiteral, 12 | Symbol, 13 | Token, 14 | TokenKind, 15 | Transition, 16 | Word, 17 | }; 18 | 19 | 20 | /// The top level lexer state. 21 | #[derive(Debug)] 22 | pub(super) struct Root; 23 | 24 | 25 | impl Root { 26 | pub fn visit(self, cursor: &Cursor) -> Transition { 27 | match cursor.peek() { 28 | // Whitespace. 29 | Some(c) if c.is_ascii_whitespace() => Transition::step(self), 30 | 31 | // Comments. 32 | Some(b'#') => Transition::step(Comment::from(self)), 33 | 34 | // String literals. 35 | Some(b'"') => Transition::step(StringLiteral::at(cursor)), 36 | 37 | // Byte literals. 38 | Some(b'\'') => Transition::step(ByteLiteral::at(cursor)), 39 | 40 | // Number literals. 41 | Some(c) if c.is_ascii_digit() => Transition::step(NumberLiteral::at(cursor)), 42 | 43 | // Identifier, keywords and word operators. 44 | Some(c) if c.is_word_start() => Transition::resume(Word::at(cursor)), 45 | 46 | // Symbols. 47 | Some(c) => match SymbolChar::from_first(c) { 48 | SymbolChar::None => Transition::error(self, Error::unexpected(c, cursor.pos())), 49 | 50 | SymbolChar::Single(TokenKind::Command) => Transition::produce( 51 | Command, 52 | Token { kind: TokenKind::Command, pos: cursor.pos() }, 53 | ), 54 | 55 | SymbolChar::Single(token) => { 56 | Transition::produce(self, Token { kind: token, pos: cursor.pos() }) 57 | } 58 | 59 | SymbolChar::Double { first } => Transition::step(Symbol::from_first(first, cursor)), 60 | }, 61 | 62 | // Eof. 63 | None => Transition::step(self), 64 | } 65 | } 66 | } 67 | 68 | 69 | impl From for State { 70 | fn from(state: Root) -> State { 71 | Self::Root(state) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/syntax/lexer/automata/word.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | Cursor, 3 | Keyword, 4 | Literal, 5 | Operator, 6 | Root, 7 | SourcePos, 8 | State, 9 | SymbolInterner, 10 | Token, 11 | TokenKind, 12 | Transition, 13 | }; 14 | 15 | 16 | /// The state for lexing identifiers, keywords and word operators. 17 | #[derive(Debug)] 18 | pub(super) struct Word { 19 | start_offset: usize, 20 | pos: SourcePos, 21 | } 22 | 23 | 24 | impl Word { 25 | pub fn at(cursor: &Cursor) -> Self { 26 | Self { start_offset: cursor.offset(), pos: cursor.pos() } 27 | } 28 | 29 | 30 | pub fn visit(self, cursor: &Cursor, interner: &mut SymbolInterner) -> Transition { 31 | // We don't need to check if the first character is a number here, because the Root 32 | // state will only transition to this state if that is the case. 33 | match cursor.peek() { 34 | // Word character. 35 | Some(c) if c.is_word() => Transition::step(self), 36 | 37 | // If we visit EOF or a non-identifier character, we should just produce. 38 | _ => { 39 | let word = &cursor.slice()[self.start_offset .. cursor.offset()]; 40 | let token = to_token(word, interner); 41 | 42 | Transition::resume_produce(Root, Token { kind: token, pos: self.pos }) 43 | } 44 | } 45 | } 46 | } 47 | 48 | 49 | impl From for State { 50 | fn from(state: Word) -> State { 51 | State::Word(state) 52 | } 53 | } 54 | 55 | 56 | pub fn to_token(word: &[u8], interner: &mut SymbolInterner) -> TokenKind { 57 | match word { 58 | // Keywords: 59 | b"let" => TokenKind::Keyword(Keyword::Let), 60 | b"if" => TokenKind::Keyword(Keyword::If), 61 | b"then" => TokenKind::Keyword(Keyword::Then), 62 | b"else" => TokenKind::Keyword(Keyword::Else), 63 | b"elseif" => TokenKind::Keyword(Keyword::ElseIf), 64 | b"end" => TokenKind::Keyword(Keyword::End), 65 | b"for" => TokenKind::Keyword(Keyword::For), 66 | b"in" => TokenKind::Keyword(Keyword::In), 67 | b"do" => TokenKind::Keyword(Keyword::Do), 68 | b"while" => TokenKind::Keyword(Keyword::While), 69 | b"function" => TokenKind::Keyword(Keyword::Function), 70 | b"return" => TokenKind::Keyword(Keyword::Return), 71 | b"break" => TokenKind::Keyword(Keyword::Break), 72 | b"self" => TokenKind::Keyword(Keyword::Self_), 73 | 74 | // Literals: 75 | b"nil" => TokenKind::Literal(Literal::Nil), 76 | b"true" => TokenKind::Literal(Literal::True), 77 | b"false" => TokenKind::Literal(Literal::False), 78 | 79 | // Operators: 80 | b"not" => TokenKind::Operator(Operator::Not), 81 | b"and" => TokenKind::Operator(Operator::And), 82 | b"or" => TokenKind::Operator(Operator::Or), 83 | 84 | // Identifier: 85 | ident => { 86 | let symbol = interner.get_or_intern(ident); 87 | TokenKind::Identifier(symbol) 88 | } 89 | } 90 | } 91 | 92 | 93 | /// Helper trait for checking if a character is a valid word constituent. 94 | pub trait IsWord { 95 | fn is_word_start(&self) -> bool; 96 | fn is_word(&self) -> bool; 97 | } 98 | 99 | 100 | impl IsWord for u8 { 101 | fn is_word_start(&self) -> bool { 102 | self.is_ascii_alphabetic() || *self == b'_' 103 | } 104 | 105 | fn is_word(&self) -> bool { 106 | self.is_ascii_alphanumeric() || *self == b'_' 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/syntax/lexer/cursor.rs: -------------------------------------------------------------------------------- 1 | use super::{Source, SourcePos}; 2 | 3 | 4 | /// A cursor for the source code. 5 | #[derive(Debug, Clone)] 6 | pub struct Cursor<'a> { 7 | input: &'a [u8], 8 | offset: usize, 9 | pos: SourcePos, 10 | } 11 | 12 | 13 | impl<'a> Cursor<'a> { 14 | pub fn pos(&self) -> SourcePos { 15 | self.pos 16 | } 17 | 18 | 19 | pub fn offset(&self) -> usize { 20 | self.offset 21 | } 22 | 23 | 24 | pub fn is_eof(&self) -> bool { 25 | self.offset == self.input.len() 26 | } 27 | 28 | 29 | pub fn peek(&self) -> Option { 30 | self.input.get(self.offset).copied() 31 | } 32 | 33 | 34 | pub fn slice(&self) -> &'a [u8] { 35 | self.input 36 | } 37 | 38 | 39 | pub fn step(&mut self) { 40 | if self.is_eof() { 41 | return; 42 | } 43 | 44 | if self.input[self.offset] == b'\n' { 45 | self.pos.line += 1; 46 | self.pos.column = 0; 47 | } else { 48 | self.pos.column += 1; 49 | } 50 | 51 | self.offset += 1; 52 | } 53 | 54 | 55 | /// Save a checkpoint in the current position. 56 | pub fn checkpoint(&self) -> Checkpoint { 57 | Checkpoint { 58 | offset: self.offset, 59 | pos: self.pos, 60 | } 61 | } 62 | 63 | 64 | /// Rollback to the given checkpoint. 65 | pub fn rollback(&mut self, checkpoint: Checkpoint) { 66 | self.offset = checkpoint.offset; 67 | self.pos = checkpoint.pos; 68 | } 69 | } 70 | 71 | 72 | impl<'a> From<&'a Source> for Cursor<'a> { 73 | fn from(source: &'a Source) -> Self { 74 | Self { 75 | input: &source.contents, 76 | offset: 0, 77 | pos: SourcePos { line: 1, column: 0, path: source.path } 78 | } 79 | } 80 | } 81 | 82 | 83 | /// A cursor checkpoint. 84 | /// This can be used to save and restore a position. 85 | #[derive(Debug, Copy, Clone)] 86 | pub struct Checkpoint { 87 | offset: usize, 88 | pos: SourcePos, 89 | } 90 | -------------------------------------------------------------------------------- /src/syntax/lexer/error/fmt.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | fmt::{self, Display}, 3 | symbol, 4 | }; 5 | use super::{Error, ErrorKind}; 6 | 7 | 8 | impl std::fmt::Display for ErrorKind { 9 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 10 | match self { 11 | Self::UnexpectedEof => "unexpected end of file".fmt(f)?, 12 | 13 | Self::Unexpected(value) => write!(f, "unexpected '{}'", (*value as char).escape_debug())?, 14 | 15 | Self::EmptyByteLiteral => "empty char literal".fmt(f)?, 16 | 17 | Self::InvalidEscapeSequence(sequence) => { 18 | write!( 19 | f, 20 | "invalid escape sequence '{}'", 21 | String::from_utf8_lossy(sequence) 22 | )?; 23 | } 24 | 25 | Self::InvalidNumber(number) => { 26 | write!(f, "invalid number '{}'", String::from_utf8_lossy(number))?; 27 | } 28 | 29 | Self::InvalidIdentifier(ident) => { 30 | write!(f, "invalid identifier '{}'", String::from_utf8_lossy(ident))?; 31 | } 32 | }; 33 | 34 | Ok(()) 35 | } 36 | } 37 | 38 | 39 | impl<'a> Display<'a> for Error { 40 | type Context = &'a symbol::Interner; 41 | 42 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 43 | write!(f, "{} - {}.", fmt::Show(self.pos, context), self.error) 44 | } 45 | } 46 | 47 | 48 | impl std::fmt::Display for Error { 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 50 | write!(f, "line {}, column {} - {}.", self.pos.line, self.pos.column, self.error) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/syntax/lexer/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use super::SourcePos; 4 | 5 | 6 | /// The kind of lexical error. 7 | #[derive(Debug)] 8 | pub enum ErrorKind { 9 | /// Unexpected end of file. 10 | UnexpectedEof, 11 | /// Unexpected character. 12 | Unexpected(u8), 13 | /// Empty byte literal (''). 14 | EmptyByteLiteral, 15 | /// Invalid escape sequence in byte literal, string literal, or argument literal. 16 | InvalidEscapeSequence(Box<[u8]>), 17 | /// Invalid number literal, both integer and floating point. 18 | InvalidNumber(Box<[u8]>), 19 | /// Invalid identifier, only possible in dollar braces (${}). 20 | InvalidIdentifier(Box<[u8]>), 21 | } 22 | 23 | 24 | /// A lexical error. 25 | #[derive(Debug)] 26 | pub struct Error { 27 | pub error: ErrorKind, 28 | pub pos: SourcePos, 29 | } 30 | 31 | 32 | impl std::error::Error for Error {} 33 | 34 | 35 | impl Error { 36 | pub fn unexpected_eof(pos: SourcePos) -> Self { 37 | Self { error: ErrorKind::UnexpectedEof, pos } 38 | } 39 | 40 | pub fn unexpected(input: u8, pos: SourcePos) -> Self { 41 | Self { error: ErrorKind::Unexpected(input), pos } 42 | } 43 | 44 | pub fn empty_byte_literal(pos: SourcePos) -> Self { 45 | Self { error: ErrorKind::EmptyByteLiteral, pos } 46 | } 47 | 48 | pub fn invalid_escape_sequence(sequence: &[u8], pos: SourcePos) -> Self { 49 | Self { 50 | error: ErrorKind::InvalidEscapeSequence(sequence.into()), 51 | pos, 52 | } 53 | } 54 | 55 | pub fn invalid_number(number: &[u8], pos: SourcePos) -> Self { 56 | Self { 57 | error: ErrorKind::InvalidNumber(number.into()), 58 | pos, 59 | } 60 | } 61 | 62 | pub fn invalid_identifier(ident: &[u8], pos: SourcePos) -> Self { 63 | Self { 64 | error: ErrorKind::InvalidIdentifier(ident.into()), 65 | pos, 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/syntax/lexer/mod.rs: -------------------------------------------------------------------------------- 1 | mod automata; 2 | mod cursor; 3 | mod error; 4 | #[cfg(test)] 5 | mod tests; 6 | mod token; 7 | 8 | use crate::symbol; 9 | use automata::Automata; 10 | use super::{Source, SourcePos}; 11 | pub use cursor::{Cursor, Checkpoint}; 12 | pub use error::{Error, ErrorKind}; 13 | pub use token::{ 14 | ArgPart, 15 | ArgUnit, 16 | ArgExpansion, 17 | CommandOperator, 18 | Keyword, 19 | Literal, 20 | Operator, 21 | Token, 22 | TokenKind 23 | }; 24 | 25 | 26 | /// The lexer for Hush source code. 27 | #[derive(Debug)] 28 | pub struct Lexer<'a, 'b>(Automata<'a, 'b>); 29 | 30 | 31 | impl<'a, 'b> Lexer<'a, 'b> { 32 | pub fn new(cursor: Cursor<'a>, interner: &'b mut symbol::Interner) -> Self { 33 | Self(Automata::new(cursor, interner)) 34 | } 35 | } 36 | 37 | 38 | impl<'a, 'b> Iterator for Lexer<'a, 'b> { 39 | type Item = Result; 40 | 41 | fn next(&mut self) -> Option { 42 | self.0.next() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/syntax/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod error; 3 | pub mod lexer; 4 | pub mod parser; 5 | mod fmt; 6 | mod source; 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | use std::cell::RefCell; 11 | 12 | use crate::symbol; 13 | pub use ast::Ast; 14 | pub use error::{Error, Errors}; 15 | use lexer::Lexer; 16 | use parser::Parser; 17 | pub use source::{Source, SourcePos}; 18 | pub use fmt::AnalysisDisplayContext; 19 | 20 | 21 | /// Syntactical analysis. 22 | #[derive(Debug)] 23 | pub struct Analysis { 24 | /// The produced AST, possibly partial if there were errors. 25 | pub ast: Ast, 26 | /// Syntax errors. 27 | pub errors: Errors, 28 | } 29 | 30 | 31 | impl Analysis { 32 | /// Perform syntax analysis in the given source. 33 | pub fn analyze(source: &Source, interner: &mut symbol::Interner) -> Self { 34 | let cursor = lexer::Cursor::from(source); 35 | let lexer = Lexer::new(cursor, interner); 36 | 37 | // Errors will be produced by the lexer and the parser alternatively. 38 | // There won't be borrow issues here because the lexer will always run a complete 39 | // iteration (producing a token or an error) before yielding to the parser. 40 | let errors = RefCell::new(Vec::new()); 41 | 42 | let tokens = lexer.filter_map(|result| match result { 43 | Ok(token) => Some(token), 44 | Err(error) => { 45 | errors.borrow_mut().push(Error::Lexer(error)); 46 | None 47 | } 48 | }); 49 | 50 | let parser = Parser::new(tokens, |error| { 51 | errors.borrow_mut().push(Error::Parser(error)) 52 | }); 53 | 54 | let statements = parser.parse(); 55 | 56 | Analysis { 57 | ast: Ast { 58 | source: source.path, 59 | statements 60 | }, 61 | errors: Errors(errors.into_inner().into()), 62 | } 63 | } 64 | 65 | 66 | /// Check if no errors occurred. 67 | pub fn is_ok(&self) -> bool { 68 | self.errors.is_empty() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/syntax/parser/error/fmt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display as _; 2 | 3 | use super::{Error, Expected, Token}; 4 | use crate::{ 5 | fmt::{self, Display}, 6 | symbol, 7 | }; 8 | 9 | 10 | impl<'a> Display<'a> for Expected { 11 | type Context = &'a symbol::Interner; 12 | 13 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 14 | match self { 15 | Self::Token(token) => { 16 | "'".fmt(f)?; 17 | token.fmt(f, context)?; 18 | "'".fmt(f) 19 | } 20 | 21 | Self::Message(msg) => msg.fmt(f), 22 | } 23 | } 24 | } 25 | 26 | 27 | impl<'a> Display<'a> for Error { 28 | type Context = &'a symbol::Interner; 29 | 30 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 31 | match self { 32 | Self::InvalidEnvAssign => "internal error: invalid env-assign".fmt(f), 33 | 34 | Self::UnexpectedEof => "unexpected end of file".fmt(f), 35 | 36 | Self::Unexpected { token: Token { kind, pos }, expected } => { 37 | write!(f, "{} - unexpected '", fmt::Show(pos, context))?; 38 | kind.fmt(f, context)?; 39 | "', expected ".fmt(f)?; 40 | expected.fmt(f, context) 41 | }, 42 | 43 | Self::EmptyCommandBlock { pos } => { 44 | write!(f, "{} - empty command block", fmt::Show(pos, context)) 45 | } 46 | } 47 | } 48 | } 49 | 50 | 51 | /// We need this in order to be able to implement std::error::Error. 52 | impl std::fmt::Display for Error { 53 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 54 | Display::fmt(self, f, &symbol::Interner::new()) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/syntax/parser/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod fmt; 2 | 3 | use super::{SourcePos, Token, TokenKind}; 4 | 5 | 6 | /// The kind of token the parser was expecting. 7 | #[derive(Debug)] 8 | pub enum Expected { 9 | Token(TokenKind), 10 | Message(&'static str), 11 | } 12 | 13 | 14 | /// A parser error. 15 | #[derive(Debug)] 16 | pub enum Error { 17 | /// Premature EOF. 18 | UnexpectedEof, 19 | /// Unexpected token. 20 | Unexpected { token: Token, expected: Expected }, 21 | /// Command blocks must have at least one command. 22 | EmptyCommandBlock { pos: SourcePos }, 23 | /// Invalid env-assign. This is a spurious error while parsing, and should be handled 24 | /// internally. 25 | InvalidEnvAssign, 26 | } 27 | 28 | 29 | impl Error { 30 | /// Create an error signaling unexpected EOF. 31 | pub fn unexpected_eof() -> Self { 32 | Self::UnexpectedEof 33 | } 34 | 35 | 36 | /// Create an error signaling an unexpected token, and what was expected. 37 | pub fn unexpected(token: Token, expected: TokenKind) -> Self { 38 | Self::Unexpected { token, expected: Expected::Token(expected) } 39 | } 40 | 41 | 42 | /// Create an error signaling an unexpected token, and a message. 43 | pub fn unexpected_msg(token: Token, message: &'static str) -> Self { 44 | Self::Unexpected { token, expected: Expected::Message(message) } 45 | } 46 | 47 | 48 | /// Create an error signaling a command block is empty. 49 | pub fn empty_command_block(pos: SourcePos) -> Self { 50 | Self::EmptyCommandBlock { pos } 51 | } 52 | } 53 | 54 | 55 | impl std::error::Error for Error {} 56 | -------------------------------------------------------------------------------- /src/syntax/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsStr, 3 | fs::File, 4 | os::unix::ffi::OsStrExt, 5 | }; 6 | 7 | use crate::{ 8 | fmt::{self, Display}, 9 | symbol::{self, Symbol}, 10 | }; 11 | 12 | 13 | /// Hush source code. 14 | #[derive(Debug)] 15 | pub struct Source { 16 | /// The origin path, may be something fictional like ``. 17 | pub path: Symbol, 18 | /// The source code. 19 | pub contents: Box<[u8]>, 20 | } 21 | 22 | 23 | impl Source { 24 | /// Load the source code from a file path. 25 | pub fn from_path(symbol: Symbol, interner: &mut symbol::Interner) -> std::io::Result { 26 | let path = OsStr::from_bytes( 27 | interner 28 | .resolve(symbol) 29 | .expect("failed to resolve path symbol") 30 | ); 31 | let file = File::open(path)?; 32 | Self::from_reader(symbol, file) 33 | } 34 | 35 | 36 | /// Load the source code from a std::io::Read. 37 | /// The path argument may be anything, including fictional paths like ``. 38 | pub fn from_reader(path: Symbol, mut reader: R) -> std::io::Result 39 | where 40 | R: std::io::Read, 41 | { 42 | let mut contents = Vec::with_capacity(512); // Expect a few characters. 43 | reader.read_to_end(&mut contents)?; 44 | 45 | Ok(Self { path, contents: contents.into() }) 46 | } 47 | } 48 | 49 | 50 | /// A human readable position in the source code. 51 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 52 | pub struct SourcePos { 53 | pub line: u32, 54 | pub column: u32, 55 | pub path: Symbol, 56 | } 57 | 58 | 59 | impl<'a> Display<'a> for SourcePos { 60 | type Context = &'a symbol::Interner; 61 | 62 | fn fmt(&self, f: &mut std::fmt::Formatter, context: Self::Context) -> std::fmt::Result { 63 | write!( 64 | f, 65 | "{} (line {}, column {})", 66 | fmt::Show(self.path, context), 67 | self.line, 68 | self.column 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/command-block-1.hsh: -------------------------------------------------------------------------------- 1 | {} # Empty command block. 2 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/command-block-2.hsh: -------------------------------------------------------------------------------- 1 | { ; } # Empty command block. 2 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/command-block-3.hsh: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | function hey(a) 4 | end 5 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/conditionals-1.hsh: -------------------------------------------------------------------------------- 1 | if true and true then 2 | # empty 3 | else 4 | # empty 5 | elseif true and true then 6 | # empty 7 | end 8 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/conditionals-2.hsh: -------------------------------------------------------------------------------- 1 | if true and true then 2 | # empty 3 | elseif true and true 4 | # empty 5 | end 6 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/dollar-1.hsh: -------------------------------------------------------------------------------- 1 | { 2 | ${1} 3 | } 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/dollar-2.hsh: -------------------------------------------------------------------------------- 1 | { 2 | ${self} 3 | } 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/dollar-3.hsh: -------------------------------------------------------------------------------- 1 | { 2 | $true 3 | } 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/expr-1.hsh: -------------------------------------------------------------------------------- 1 | @[ 2 | hai: 1, 3 | b: [ 5 * ], # missing operand here 4 | c: 3 5 | ] 6 | 7 | function b() 8 | std.println(hai) 9 | end 10 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/expr-2.hsh: -------------------------------------------------------------------------------- 1 | fun(1, hello., 3,) # missing dot access identifier. 2 | 5 + 5 3 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/expr-3.hsh: -------------------------------------------------------------------------------- 1 | function hai() 2 | return (5 + (2) # Missing close parens here. 3 | end 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/for-loop-1.hsh: -------------------------------------------------------------------------------- 1 | for arg in do 2 | 1 3 | end 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/for-loop-is-not-an-expression.hsh: -------------------------------------------------------------------------------- 1 | result = for var in iter do var() end 2 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/funcall-1.hsh: -------------------------------------------------------------------------------- 1 | call() 2 | dict[field](arg # missing parens here 3 | fun(many)(chained)[field](calls) 4 | fun(many).field(chained).field.calls() 5 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/funcall-2.hsh: -------------------------------------------------------------------------------- 1 | call() 2 | dict[field](arg) 3 | fun(many)(chained)[field](calls.) # missing dot access identifier here. 4 | fun(many).field(chained).field.calls() 5 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/statement-1.hsh: -------------------------------------------------------------------------------- 1 | . # invalid dot access operator. 2 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/while-loop-1.hsh: -------------------------------------------------------------------------------- 1 | while arg 2 | fun() 3 | end 4 | -------------------------------------------------------------------------------- /src/syntax/tests/data/negative/while-loop-is-not-an-expression.hsh: -------------------------------------------------------------------------------- 1 | let result = while true do end 2 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/command-block.hsh: -------------------------------------------------------------------------------- 1 | { 2 | hey you; 3 | out there on the wall << 'some input string' 2>1 1>2 >> file > $file ? 4 | | and pipes ? 5 | | can be 2>1 6 | # interleaved with comments 7 | | fun; 8 | } 9 | 10 | let error = ${ 11 | echo $args' aren\'t '$boring" even when \$'$double' quoted"; 12 | this command does not exist 13 | }[1] 14 | 15 | &{ 16 | go async 17 | }.join() 18 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/conditionals.hsh: -------------------------------------------------------------------------------- 1 | if true and true then 2 | # empty 3 | end 4 | 5 | if true and true then 6 | # empty 7 | else 8 | # empty 9 | end 10 | 11 | if true and true then 12 | # empty 13 | elseif true and true then 14 | # empty 15 | end 16 | 17 | if true and true then 18 | # empty 19 | elseif true and true then 20 | # empty 21 | else 22 | # empty 23 | end 24 | 25 | if true and true then 26 | if true and true then 27 | # empty 28 | elseif true and true then 29 | # empty 30 | else 31 | # empty 32 | end 33 | elseif true and true then 34 | if true and true then 35 | # empty 36 | elseif true and true then 37 | # empty 38 | else 39 | # empty 40 | end 41 | else 42 | if true and true then 43 | # empty 44 | elseif true and true then 45 | # empty 46 | else 47 | # empty 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/funcall.hsh: -------------------------------------------------------------------------------- 1 | let condition = call() 2 | dict[field](arg) 3 | fun(many)(chained)[field](calls) 4 | fun(many).field(chained).field.calls() 5 | function () 6 | if condition then 7 | return function() 8 | return "foo" 9 | end 10 | else 11 | return function() 12 | return "bar" 13 | end 14 | end 15 | end()() 16 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/keywords.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | let var = if not false then 3 | nil 4 | else 5 | self 6 | end 7 | 8 | while true and false or true do 9 | break 10 | end 11 | 12 | for var in val do 13 | return 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/arrays.hsh: -------------------------------------------------------------------------------- 1 | [ 2 | nil, 3 | true, 4 | [ "nested" ], 5 | [ [[[[[]]]]], [ "ultra", ["nested"] ] ], 6 | ] 7 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/basic.hsh: -------------------------------------------------------------------------------- 1 | [ 2 | nil, 3 | true, 4 | false, 5 | 0, 6 | -1, 7 | 9223372036854775807, 8 | -9223372036854775807, 9 | 524.08, 10 | 4848.0, 11 | 0.2464, 12 | 3.14e25, 13 | 3.14e0, 14 | ] 15 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/chars.hsh: -------------------------------------------------------------------------------- 1 | [ 2 | 'a', 3 | '\0', 4 | '\n', 5 | '\t', 6 | '\'', 7 | '\"', 8 | '\\', 9 | ] 10 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/dicts.hsh: -------------------------------------------------------------------------------- 1 | @[ 2 | a: b, 3 | b: 1, 4 | c: 1.0, 5 | d: "string", 6 | e: @[ 7 | nested: @[ _: @[ _: @[ _: @[ _: @[] ] ] ] ], 8 | ], 9 | f: [ "mixed with arrays", @[ x: y ] ] 10 | ] 11 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/functions.hsh: -------------------------------------------------------------------------------- 1 | function () 2 | function(a, b, c) 3 | "here" 4 | "be" 5 | "my body" 6 | end 7 | 8 | function (a,) 9 | [ 10 | function () function () function () function () function () end end end end end, 11 | @[ fun: function () end ] 12 | ] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/literals/strings.hsh: -------------------------------------------------------------------------------- 1 | "here" 2 | "be" 3 | "some" 4 | "funny strings \n\t\0\\\'\"" 5 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/operators.hsh: -------------------------------------------------------------------------------- 1 | let negation = not not not not not false 2 | let negative = - - - 1 3 | let equality = 1 == 2 and 2 != 3 4 | let ord = 1 >= 2 or 3 <= 4 and 1 < 2 or 5 > 6 5 | let concat = "hello" ++ " " ++ "world" 6 | let arith = 1 + 2 - 3 * 4 / 5 % 6 7 | let try = 1? + call()? 8 | 9 | let expr = not true and [ nil, true, 0][1 * 1] == @[ fun: function (arg) return arg end ].fun(nil) 10 | -------------------------------------------------------------------------------- /src/syntax/tests/data/positive/variables.hsh: -------------------------------------------------------------------------------- 1 | let var 2 | let another_var = (if true then ['a'] else ['b'] end)[0] 3 | var = 5 * if false then 0 else 1 end + 7 4 | -------------------------------------------------------------------------------- /src/syntax/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | path::Path, 4 | os::unix::ffi::OsStrExt, 5 | }; 6 | 7 | use crate::{fmt, symbol, syntax::AnalysisDisplayContext, tests}; 8 | use super::{Analysis, Source}; 9 | 10 | 11 | fn test_dir(path: P, mut check: F) -> io::Result<()> 12 | where 13 | P: AsRef, 14 | F: FnMut(&Analysis) -> bool, 15 | { 16 | let mut interner = symbol::Interner::new(); 17 | 18 | tests::util::test_dir( 19 | path, 20 | move |path, file| { 21 | let path_symbol = interner.get_or_intern(path.as_os_str().as_bytes()); 22 | let source = Source::from_reader(path_symbol, file)?; 23 | let analysis = Analysis::analyze(&source, &mut interner); 24 | 25 | if !check(&analysis) { 26 | panic!("{}", fmt::Show( 27 | analysis, 28 | AnalysisDisplayContext { 29 | max_errors: None, 30 | interner: &interner, 31 | } 32 | )); 33 | } 34 | 35 | Ok(()) 36 | } 37 | ) 38 | } 39 | 40 | 41 | #[test] 42 | fn test_examples() -> io::Result<()> { 43 | test_dir( 44 | "examples/hush", 45 | |analysis| analysis.errors.is_empty(), 46 | ) 47 | } 48 | 49 | 50 | #[test] 51 | fn test_positive() -> io::Result<()> { 52 | test_dir( 53 | "src/syntax/tests/data/positive", 54 | |analysis| analysis.errors.is_empty(), 55 | ) 56 | } 57 | 58 | 59 | #[test] 60 | fn test_negative() -> io::Result<()> { 61 | test_dir( 62 | "src/syntax/tests/data/negative", 63 | |analysis| !analysis.errors.is_empty(), 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/term/color.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | fmt::{self, Debug, Display}, 4 | }; 5 | 6 | pub use termion::color::{Blue, Green, Red, Yellow}; 7 | 8 | 9 | thread_local! { 10 | static IS_TTY: bool = termion::is_tty(&io::stdout()) 11 | && termion::is_tty(&io::stderr()); 12 | } 13 | 14 | 15 | macro_rules! tty_fmt { 16 | ($f: expr, $open: expr, $value: expr, $close: expr) => { 17 | IS_TTY.with( 18 | |&is_tty| if is_tty { 19 | write!($f, "{}", $open)?; 20 | $value.fmt($f)?; 21 | write!($f, "{}", $close) 22 | } else { 23 | $value.fmt($f) 24 | } 25 | ) 26 | } 27 | } 28 | 29 | 30 | /// Paint the foreground with a given color when formatting the value. 31 | pub struct Fg(pub C, pub T); 32 | 33 | 34 | impl Debug for Fg 35 | where 36 | C: termion::color::Color + Copy, 37 | T: Debug, 38 | { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | tty_fmt!( 41 | f, 42 | termion::color::Fg(self.0), 43 | self.1, 44 | termion::color::Fg(termion::color::Reset) 45 | ) 46 | } 47 | } 48 | 49 | 50 | impl Display for Fg 51 | where 52 | C: termion::color::Color + Copy, 53 | T: Display, 54 | { 55 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 56 | tty_fmt!( 57 | f, 58 | termion::color::Fg(self.0), 59 | self.1, 60 | termion::color::Fg(termion::color::Reset) 61 | ) 62 | } 63 | } 64 | 65 | 66 | /// Paint with a given style. 67 | pub struct Style(pub S, pub T); 68 | 69 | 70 | impl Debug for Style 71 | where 72 | S: Display, 73 | T: Debug, 74 | { 75 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 76 | tty_fmt!( 77 | f, 78 | self.0, 79 | self.1, 80 | termion::style::Reset 81 | ) 82 | } 83 | } 84 | 85 | 86 | impl Display for Style 87 | where 88 | S: Display, 89 | T: Display, 90 | { 91 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 92 | tty_fmt!( 93 | f, 94 | self.0, 95 | self.1, 96 | termion::style::Reset 97 | ) 98 | } 99 | } 100 | 101 | 102 | /// Paint with a bold style. 103 | pub struct Bold(pub T); 104 | 105 | 106 | impl Debug for Bold 107 | where 108 | T: Debug, 109 | { 110 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 111 | Style(termion::style::Bold, &self.0).fmt(f) 112 | } 113 | } 114 | 115 | 116 | impl Display for Bold 117 | where 118 | T: Display, 119 | { 120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 121 | Style(termion::style::Bold, &self.0).fmt(f) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/term/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod util; 2 | -------------------------------------------------------------------------------- /src/tests/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | fs::{self, File}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | 8 | pub fn test_dir(path: P, mut test: F) -> io::Result<()> 9 | where 10 | P: AsRef, 11 | F: FnMut(&Path, File) -> io::Result<()>, 12 | { 13 | let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 14 | dir.push(path); 15 | 16 | fn run(dir: &Path, test: &mut F) -> io::Result<()> 17 | where 18 | F: FnMut(&Path, File) -> io::Result<()>, 19 | { 20 | for entry in fs::read_dir(dir)? { 21 | let path = entry?.path(); 22 | 23 | if path.is_dir() { 24 | run(&path, test)?; 25 | } else { 26 | let file = File::open(&path)?; 27 | test(&path, file)?; 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | run(&dir, &mut test) 35 | } 36 | -------------------------------------------------------------------------------- /syntax-highlight/emacs/hush-mode.el: -------------------------------------------------------------------------------- 1 | ;; Uncomment this to make defvar override previous values. 2 | ;; (mapc #'unintern '(hush-keywords 3 | ;; hush-mode-syntax-table 4 | ;; hush-font-lock-keywords 5 | ;; hush-font-lock 6 | ;; hush-mode-map)) 7 | 8 | (defvar hush-keywords 9 | '("let" "if" "then" "else" "elseif" "end" "for" "in" "do" "while" "function" "return" 10 | "not" "and" "or" "true" "false" "nil" "break" "self")) 11 | 12 | (defvar hush-mode-syntax-table 13 | (with-syntax-table (copy-syntax-table) 14 | ;; comment syntax: begins with "#", ends with "\n" 15 | (modify-syntax-entry ?# "<") 16 | (modify-syntax-entry ?\n ">") 17 | 18 | ;; main string syntax: bounded by ' or " 19 | (modify-syntax-entry ?\' "\"") 20 | (modify-syntax-entry ?\" "\"") 21 | 22 | ;; single-character binary operators: punctuation 23 | (modify-syntax-entry ?+ ".") 24 | (modify-syntax-entry ?- ".") 25 | (modify-syntax-entry ?* ".") 26 | (modify-syntax-entry ?/ ".") 27 | (modify-syntax-entry ?% ".") 28 | (modify-syntax-entry ?> ".") 29 | (modify-syntax-entry ?< ".") 30 | (modify-syntax-entry ?= ".") 31 | (modify-syntax-entry ?! ".") 32 | 33 | (syntax-table)) 34 | "`hush-mode' syntax table.") 35 | 36 | (defvar hush-font-lock-keywords 37 | (concat "\\<\\(" (regexp-opt hush-keywords) "\\)\\>" )) 38 | 39 | (defvar hush-font-lock 40 | `((,hush-font-lock-keywords . font-lock-keyword-face))) 41 | 42 | (defun hush-font-lock-setup () 43 | "Set up Hush font lock." 44 | (setq-local font-lock-defaults '((hush-font-lock) nil t))) 45 | 46 | (defvar hush-mode-map (make-sparse-keymap) "The keymap for Hush scripts") 47 | ;; (define-key hush-mode-map (kbd "C-c t") 'find-file) 48 | 49 | (define-derived-mode hush-mode lua-mode "hush" () 50 | :syntax-table hush-mode-syntax-table 51 | (setq-local comment-start "# " 52 | comment-start-skip "##*[ \t]*" 53 | comment-use-syntax t) 54 | (hush-font-lock-setup)) 55 | 56 | (add-to-list 'auto-mode-alist '("\\.hsh\\'" . hush-mode)) 57 | 58 | 59 | ;; Babel: 60 | (defun org-babel-execute:hush (body params) 61 | "Execute a block of Hush code with org-babel." 62 | (org-babel-eval "hush" body)) 63 | -------------------------------------------------------------------------------- /syntax-highlight/pygments/lexer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hush-shell/hush/560c33a2dc8bf967b4fb0c80e3f18ca8934615ff/syntax-highlight/pygments/lexer/__init__.py -------------------------------------------------------------------------------- /syntax-highlight/pygments/lexer/hush.py: -------------------------------------------------------------------------------- 1 | from pygments.lexer import RegexLexer, include, default, combined 2 | from pygments.token import Text, Comment, Operator, Keyword, Name, String, \ 3 | Number, Punctuation 4 | 5 | 6 | class HushLexer(RegexLexer): 7 | """ 8 | For `Hush ` scripts. 9 | """ 10 | 11 | name = 'Hush' 12 | aliases = ['hush'] 13 | filenames = ['*.hsh'] 14 | mimetypes = ['text/x-hush', 'application/x-hush'] 15 | 16 | _comment = r'(?:#.*$)' 17 | _space = r'(?:\s+)' 18 | _s = r'(?:%s|%s)' % (_comment, _space) 19 | _name = r'(?:[^\W\d]\w*)' 20 | 21 | tokens = { 22 | 'root': [ 23 | # Hush allows a file to start with a shebang. 24 | (r'#!.*', Comment.Preproc), 25 | default('base'), 26 | ], 27 | 'ws': [ 28 | (_comment, Comment.Single), 29 | (_space, Text), 30 | ], 31 | 'base': [ 32 | include('ws'), 33 | 34 | (r'(?i)0x[\da-f]*(\.[\da-f]*)?(p[+-]?\d+)?', Number.Hex), 35 | (r'(?i)(\d*\.\d+|\d+\.\d*)(e[+-]?\d+)?', Number.Float), 36 | (r'(?i)\d+e[+-]?\d+', Number.Float), 37 | (r'\d+', Number.Integer), 38 | 39 | # multiline strings 40 | # (r'(?s)\[(=*)\[.*?\]\1\]', String), 41 | 42 | # (r'::', Punctuation, 'label'), 43 | # (r'\.{3}', Punctuation), 44 | (r'[\?\$!=<>{}|+\-*/%]+', Operator), 45 | (r'[\[\]().,:;]|@\[', Punctuation), 46 | (r'(and|or|not)\b', Operator.Word), 47 | 48 | (r'(break|self|do|else|elseif|end|for|if|in|return|then|while)\b', Keyword.Reserved), 49 | (r'(let)\b', Keyword.Declaration), 50 | (r'(true|false|nil)\b', Keyword.Constant), 51 | 52 | (r'(function)\b', Keyword.Reserved, 'funcname'), 53 | 54 | (r'[A-Za-z_]\w*(\.[A-Za-z_]\w*)?', Name), 55 | 56 | ("'", String.Char, combined('stringescape', 'sqs')), # TODO 57 | ('"', String.Double, combined('stringescape', 'dqs')) 58 | ], 59 | 60 | 'funcname': [ 61 | include('ws'), 62 | (_name, Name.Function, '#pop'), 63 | # inline function 64 | (r'\(', Punctuation, '#pop'), 65 | ], 66 | 67 | 'stringescape': [ 68 | (r'\\([abfnrtv\\"\']|[\r\n]{1,2}|z\s*|x[0-9a-fA-F]{2}|\d{1,3}|' 69 | r'u\{[0-9a-fA-F]+\})', String.Escape), 70 | ], 71 | 72 | 'sqs': [ 73 | (r"'", String.Single, '#pop'), 74 | (r"[^\\']+", String.Single), 75 | ], 76 | 77 | 'dqs': [ 78 | (r'"', String.Double, '#pop'), 79 | (r'[^\\"]+', String.Double), 80 | ] 81 | } 82 | 83 | def get_tokens_unprocessed(self, text): 84 | for index, token, value in RegexLexer.get_tokens_unprocessed(self, text): 85 | if token is Name: 86 | if '.' in value: 87 | a, b = value.split('.') 88 | yield index, Name, a 89 | yield index + len(a), Punctuation, '.' 90 | yield index + len(a) + 1, Name, b 91 | continue 92 | yield index, token, value 93 | -------------------------------------------------------------------------------- /syntax-highlight/pygments/readme.org: -------------------------------------------------------------------------------- 1 | * Custom /Pygments/ lexer for /Hush/ 2 | This is a custom /Pygments/ lexer for /Hush/, allowing for instance one to write /Latex/ 3 | documents with proper syntax highlight. 4 | ** Installation 5 | To build and install, use the following command: 6 | #+begin_src bash 7 | python setup.py install --user 8 | #+end_src 9 | -------------------------------------------------------------------------------- /syntax-highlight/pygments/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='hushlexer', 5 | packages=find_packages(), 6 | entry_points = 7 | ''' 8 | [pygments.lexers] 9 | hushlexer = lexer.hush:HushLexer 10 | ''', 11 | ) 12 | -------------------------------------------------------------------------------- /syntax-highlight/vis/README.md: -------------------------------------------------------------------------------- 1 | # hush.lua 2 | 3 | Vis editor syntax highlighting for _Hush_. 4 | 5 | ## Installation 6 | 7 | Copy hush.lua into $XDG\_CONFIG\_HOME/vis/lexers, copy hush\_detect.lua into $XDG\_CONFIG\_HOME/vis, and add require("hush\_detect") to your visrc. 8 | -------------------------------------------------------------------------------- /syntax-highlight/vis/hush.lua: -------------------------------------------------------------------------------- 1 | -- ? LPeg lexer. 2 | 3 | local l = require('lexer') 4 | local token, word_match = l.token, l.word_match 5 | local P, R, S = lpeg.P, lpeg.R, lpeg.S 6 | 7 | local M = {_NAME = 'hush'} 8 | 9 | local comment = token(l.COMMENT, '#' * l.nonnewline_esc^0) 10 | 11 | local constant = token(l.CONSTANT, word_match{ 12 | 'true', 'false', 13 | }) 14 | 15 | local identifier = token(l.IDENTIFIER, l.word) 16 | 17 | local keyword = token(l.KEYWORD, word_match{ 18 | 'let', 'if', 'then', 'else', 'elseif', 'end', 'for', 'in', 'do', 'while', 19 | 'function', 'return', 'not', 'and', 'or', 'true', 'false', 'nil', 'break', 20 | 'self', 21 | }) 22 | 23 | local operator = token(l.OPERATOR, word_match{ 24 | 'and', 'or', 'not', 25 | } + S('+-/*%<>!=[]')) 26 | 27 | local number = token(l.NUMBER, l.float + l.integer) 28 | 29 | local sq_str = l.delimited_range("'", true) 30 | local dq_str = l.delimited_range('"', true) 31 | local string = token(l.STRING, sq_str + dq_str) 32 | 33 | local type = token(l.TYPE, word_match{ 34 | 'int', 'char', 'float', 'string', 'bool', 'array', 'dict', 35 | }) 36 | 37 | local ws = token(l.WHITESPACE, l.space^1) 38 | 39 | M._rules = { 40 | {'constant', constant}, 41 | {'comment', comment}, 42 | {'keyword', keyword}, 43 | {'number', number}, 44 | {'operator', operator}, 45 | {'string', string}, 46 | {'type', type}, 47 | {'whitespace', ws}, 48 | {'identifier', identifier}, 49 | } 50 | 51 | return M 52 | -------------------------------------------------------------------------------- /syntax-highlight/vis/hush_detect.lua: -------------------------------------------------------------------------------- 1 | vis.ftdetect.filetypes.hush = { 2 | ext = { "%.hsh$" }, 3 | } 4 | -------------------------------------------------------------------------------- /syntax-highlight/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /syntax-highlight/vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "#" 5 | }, 6 | // symbols used as brackets 7 | "brackets": [ 8 | ["{", "}"], 9 | ["[", "]"], 10 | ["@[", "]"], 11 | ["(", ")"] 12 | ], 13 | // symbols that are auto closed when typing 14 | "autoClosingPairs": [ 15 | ["{", "}"], 16 | ["[", "]"], 17 | ["@[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"] 21 | ], 22 | // symbols that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["@[", "]"], 27 | ["(", ")"], 28 | ["\"", "\""], 29 | ["'", "'"] 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /syntax-highlight/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hush", 3 | "publisher": "hush-vscode", 4 | "repository": "https://github.com/gahag/hush", 5 | "displayName": "Hush", 6 | "description": "Hush syntax highlighting", 7 | "version": "0.0.1", 8 | "engines": { 9 | "vscode": "^1.65.0" 10 | }, 11 | "categories": [ 12 | "Programming Languages" 13 | ], 14 | "contributes": { 15 | "languages": [{ 16 | "id": "hush", 17 | "aliases": ["Hush", "hush"], 18 | "extensions": [".hsh"], 19 | "configuration": "./language-configuration.json" 20 | }], 21 | "grammars": [{ 22 | "language": "hush", 23 | "scopeName": "source.hush", 24 | "path": "./syntaxes/hush.tmLanguage.json" 25 | }] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /syntax-highlight/vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension. 7 | * `syntaxes/hush.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization. 8 | * `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets. 9 | 10 | ## Get up and running straight away 11 | 12 | * Make sure the language configuration settings in `language-configuration.json` are accurate. 13 | * Press `F5` to open a new window with your extension loaded. 14 | * Create a new file with a file name suffix matching your language. 15 | * Verify that syntax highlighting works and that the language configuration settings are working. 16 | 17 | ## Make changes 18 | 19 | * You can relaunch the extension from the debug toolbar after making changes to the files listed above. 20 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 21 | 22 | ## Add more language features 23 | 24 | * To add features such as intellisense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs 25 | 26 | ## Install your extension 27 | 28 | * To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. 29 | * To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension. 30 | --------------------------------------------------------------------------------