├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── bin ├── log-filter ├── mag ├── mag-dynamic ├── magc ├── magvm-dynamic └── rpython-compile ├── doc ├── PROpresentation.sty ├── comment.cut ├── ipsj.cls ├── ipsjpref.sty ├── ipsjsort-e.bst ├── ipsjsort.bst ├── ipsjtech.sty ├── ipsjunsrt-e.bst ├── ipsjunsrt.bst ├── paper.bib └── paper.tex ├── example └── prime.mag ├── lib ├── mag │ └── prelude.mag ├── magc │ ├── ast.rb │ ├── cli.rb │ ├── compiler.rb │ ├── env.rb │ ├── free_vars.rb │ ├── index.rb │ ├── lexer.rb │ ├── log.rb │ ├── matcher.rb │ ├── parser.rb │ ├── skeleton.rb │ ├── tree.rb │ └── value.rb └── magvm │ ├── actions.py │ ├── base.py │ ├── channel.py │ ├── const.py │ ├── debug.py │ ├── env.py │ ├── frame.py │ ├── inst.py │ ├── intrinsic.py │ ├── labels.py │ ├── load.py │ ├── machine.py │ ├── proc.py │ ├── shuffle.py │ ├── spawn.py │ ├── status.py │ ├── symbol.py │ ├── table.py │ ├── targetmagritte.py │ ├── targettest.py │ ├── util.py │ └── value.py ├── log └── .gitkeep ├── notes ├── guarantees └── simple.rb ├── old-spec ├── channel_spec.rb ├── code_spec.rb ├── env_spec.rb ├── free_vars_spec.rb ├── interpret │ ├── basic_spec.rb │ ├── block_spec.rb │ ├── cond_spec.rb │ ├── env_spec.rb │ ├── example_spec.rb │ ├── interrupt_spec.rb │ ├── lambda_spec.rb │ ├── lexical_spec.rb │ └── prelude_spec.rb ├── lexer_spec.rb ├── matcher_spec.rb ├── parser_spec.rb ├── skeleton_spec.rb ├── spec_helper.rb └── support │ ├── abstract.rb │ ├── focus.rb │ ├── infinity.rb │ ├── interpret_helpers.rb │ └── rescuing.rb ├── test ├── framework.mag ├── mag │ ├── basic.mag │ ├── channels.mag │ ├── concurrent.mag │ ├── lambda.mag │ ├── oop.mag │ ├── process.mag │ └── recursion.mag └── test.mag └── vim └── syntax └── mag.vim /.gitignore: -------------------------------------------------------------------------------- 1 | # tex 2 | 3 | *.aux 4 | *.log 5 | *.dvi 6 | *.pdf 7 | *.out 8 | *.bbl 9 | /tmp 10 | 11 | # latexmk 12 | *.blg 13 | *.fdb_latexmk 14 | *.fls 15 | 16 | # vim 17 | .init.vim 18 | 19 | # python 20 | *.pyc 21 | /target*-c 22 | 23 | # magritte 24 | *.magc 25 | *.magx 26 | 27 | # build 28 | /build 29 | /bin/magvm 30 | 31 | # logs 32 | /log 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/pypy"] 2 | path = vendor/pypy 3 | url = https://github.com/mozillazg/pypy 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # use the correct latex and bibtex 2 | UPLATEXMK_OPTS += -pdfdvi 3 | UPLATEXMK_OPTS += -latex=uplatex 4 | UPLATEXMK_OPTS += -latexoption='-shell-escape' 5 | UPLATEXMK_OPTS += -latexoption='-kanji=utf8' 6 | UPLATEXMK_OPTS += -e '$$bibtex="pbibtex"' #; $$dvipdf="dvipdfmx %O %S"' 7 | # don't open a viewer 8 | UPLATEXMK_OPTS += -view=none 9 | 10 | PDFLATEXMK_OPTS += -pdf 11 | PDFLATEXMK_OPTS += -latexoption='-shell-escape' 12 | PDFLATEXMK_OPTS += -usepretex='\def\sigplan{1}' 13 | PDFLATEXMK_OPTS += -view=none 14 | 15 | CLEAN += doc/paper.pdf doc/paper.log doc/paper.aux doc/paper.out doc/paper-autopp* doc/paper.bbl doc/paper.dvi 16 | 17 | all: vm 18 | 19 | .PHONY: open-doc 20 | open-doc: doc/paper.pdf 21 | open "$^" & 22 | 23 | .PHONY: doc 24 | doc: doc/paper.pdf 25 | 26 | %.pdf: %.tex %.bib 27 | latexmk $(LATEXMK_OPTS) -output-directory=$(dir $<) $< 28 | 29 | .PHONY: sigpro-watch 30 | sigpro-watch: 31 | latexmk -pvc $(UPLATEXMK_OPTS) -output-directory=doc doc/paper.tex 32 | 33 | .PHONY: sigplan-watch 34 | sigplan-watch: 35 | latexmk -pvc $(PDFLATEXMK_OPTS) -output-directory=doc doc/paper.tex 36 | 37 | .PHONY: clean 38 | clean: 39 | rm -f -- $(CLEAN) 40 | 41 | VM_BIN=./bin/magvm 42 | CLEAN += $(VM_BIN) 43 | 44 | $(VM_BIN): lib/magvm/*.py 45 | ./bin/rpython-compile ./lib/magvm/targetmagritte.py 46 | mv targetmagritte-c $(VM_BIN) 47 | 48 | TEST_FILE=./test/test.mag 49 | 50 | DYNAMIC ?= 0 51 | 52 | .PHONY: vm 53 | vm: $(VM_BIN) 54 | 55 | .PHONY: test 56 | test: $(VM_BIN) $(TEST_FILE) 57 | ./bin/mag $(TEST_FILE) 58 | 59 | .PHONY: test-dynamic 60 | test-dynamic: $(TEST_FILE) 61 | ./bin/mag-dynamic $(TEST_FILE) 62 | 63 | CLEAN += **/*.magc **/*.magx 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magritte 2 | 3 | Magritte is a shell language with a design firmly centered around pipes. The full specification can be found in [my thesis](http://files.jneen.net/academic/thesis.pdf). 4 | 5 | The current repository represents an attempt to implement a JIT interpreter for Magritte using RPython. The Ruby interpreter mentioned in the paper can be found on the branch [old-implementation](https://github.com/prg-titech/magritte/tree/old-implementation). 6 | 7 | Both implementations are still very unstable, so please do not use this code for anything critical. 8 | 9 | ## Building 10 | 11 | * Depends on Ruby 2.6+ and Python 2.7 (yes, Python 2, it's required for RPython). There are no gem dependencies or Python package dependencies. 12 | 13 | * There is, however, one submodule - make sure you have this fetched. Either `git clone --recursive` this repository, or run `git submodule update --init` in the project root. 14 | 15 | * Run `make test` to compile and run the tests. 16 | 17 | * Run `make test-dynamic` to run the tests without compiling, using python2 directly. 18 | 19 | * After building, you can use ./bin/mag and ./bin/mag-dynamic to run magritte files directly. 20 | 21 | ## Architecture 22 | 23 | The implementation is split into three parts: 24 | 25 | * `lib/magc`, a compiler written in Ruby that generates Magritte bytecode 26 | * `lib/magvm`, a virtual machine written in RPython, which interprets the bytecode 27 | * `lib/mag`, the standard library, written in Magritte itself. 28 | 29 | We don't plan on keeping the Ruby dependency - this currently only implements the frontend of the language, and once the language is stable enough we will rewrite those components in Magritte. The RPython is here to stay. 30 | 31 | ## `magc` 32 | 33 | `magc` is a compiler frontend written in Ruby, consisting of a chain of transformations. The compiler pipeline is: 34 | 35 | * `lexer.rb`: `Text -> Tokens` 36 | * `skeleton.rb`: `Tokens -> Skeleton` 37 | * `parser.rb`: `Skeleton -> AST` (AST is defined in `ast.rb`) 38 | * `compiler.rb`: AST -> Bytecode 39 | 40 | Running `magc my-file.mag` will generate two new files, `my-file.magc` and `my-file.magx`. The `.magc` file is the binary representation of the bytecode. The `.magx` file will contain a decompilation: a human-readable representation of the generated bytecode. These files are regenerated automatically if Magritte detects they are stale (i.e. older than the source file). 41 | 42 | The instructions for the bytecode are documented in `lib/magvm/inst.py`. 43 | 44 | A bytecode file consists of four sections: 45 | 46 | * A list of all constant strings and numbers 47 | * A list of all symbols in use 48 | * A table of named labels for debugging purposes 49 | * A list of all instructions and their integer arguments 50 | 51 | The decompiled file does not contain the labels table, instead opting to mark the labels directly in the bytecode. 52 | 53 | ## `magvm` 54 | 55 | `magvm` is an interpreter for the `magc` bytecode format. It is written in RPython, which is a subset of Python that can compile to native code. Because of this, it can be either run as a native binary (much faster), or run directly as Python code (much more flexible for debugging, doesn't require a slow compilation step). 56 | 57 | Running `make test` will automatically compile the vm into `./bin/magvm`, and use it to run the test suite (`test/test.mag`). Running `make test-dynamic`, on the other hand, will not compile any RPython code, but instead run the interpreter as regular Python. `./bin/magvm-dynamic` and `./bin/mag-dynamic` do the same. 58 | 59 | The machine (`machine.py`) is an object that keeps track of multiple Proc objects (`proc.py`), stepping each active process forward independently. Each process contains its own call stack, which is simply a list of Frame objects (`frame.py`). The frame implements a basic stack machine - instructions can push or pop values (`value.py`) from the current frame's stack. 60 | 61 | A frame steps forward in the usual way, by incrementing the program counter and evaluating one instruction according to its action (`actions.py`). Instruction actions have full access to the frame, and can push/pop values, change the program counter (`jump` etc), and more. 62 | 63 | Another way that native code can be run is through intrinsic functions (`intrinsic.py`), which are functions implemented in the VM itself, which can be called like normal Magritte functions. These use the syntax `@!intrinsic-name`, and are only available in files that declare `@allow-intrinsics` at the top-level. Currently the only file that does this is `lib/mag/prelude.mag`, which exports normal functions that use the intrinsics. Intrinsics also have full access to the frame and its process. 64 | 65 | Some intrinsics, like `@!for` and `@!get`, cause a read or write on a channel. In this case, if there are no values ready in the channel, the process will change state to Proc.WAITING. This will cause it to be skipped over by the machine until the state changes. 66 | 67 | This will most likely happen in the resolve phase: Periodically, the machine will perform handoffs of values between processes. This involves calling `.resolve()` on each channel (`channel.py`), which causes processes to be moved into the RUNNING or INTERRUPTED states. 68 | 69 | ## Debugging tools 70 | 71 | You can enable debugging with the `MAGRITTE_DEBUG` environment variable. If set to 1 or 2, debug logs will go to stdout or stderr, respectively. If set to a file path, logs will be written to the specified file. The default is to write to `log/magritte.debug.log`. 72 | 73 | Since the debug logs are a bit of a firehose, they are disabled by default. To enable them for a specific piece of Magritte code, use the Magritte functions `vm-debug on` and `vm-debug off` to control which piece of execution is being debugged. 74 | 75 | Often it is necessary to further filter the logs, and for that we provide `./bin/log-filter`, which will filter a Magritte debug log according to specific "views": 76 | 77 | * View channel registrations/deregistrations with `log-view channel -c ` 78 | 79 | * View only the activity of a single proc (as well as all resolve and check phases) with `log-view proc `, e.g. `log-view proc 2` to view only ``. 80 | 81 | When running in interpreted mode, you can use the intrinsic `@!vm-debugger` to drop to a Python shell, where you will have access to the current frame and any arguments you pass in. Using this in compiled mode results in a warning. This shell is also available elsewhere in the VM through the `open_shell` Python function from the `debug` module. 82 | -------------------------------------------------------------------------------- /bin/log-filter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ -t 0 ]] && exec < log/magritte.debug.log 4 | 5 | log-view::channel() { 6 | local side='(reader|writer)' 7 | local channel='' 8 | while [[ $# -gt 0 ]]; do 9 | a="$1"; shift 10 | case "$a" in 11 | -r) side=reader ;; 12 | -w) side=writer ;; 13 | -rw) side='(reader|writer)' ;; 14 | [0-9]*) channel="$a" 15 | esac 16 | done 17 | 18 | egrep "_$side -?[0-9]+ 11 | %num = (mul %b %num | into add %d) 12 | ) 13 | put %num 14 | ) 15 | 16 | # Generate a random number having n bits. Note that the function 17 | # breaks when n < 3 because we want to ensure that the leading and 18 | # trailing bit is set to 1. Of course, nobody should actually call the 19 | # function with n < 3 because that defeats the purpose of the function. 20 | (gen-number ?n) = ((put 1; (gen-bits (add %n -2)); put 1) | from-base 2) 21 | 22 | # In Miller-rabin we want to factor a number num = (2^r)*d 23 | # and learn what r and d is. This function returns r and d 24 | (miller-rabin-factoring ?num ?r) = ( 25 | eq 0 (mod 2 %num) && miller-rabin-factoring (div 2 %num) (inc %r) 26 | !! put %r %num 27 | ) 28 | 29 | # Check if any of the first r elements in the sequence x{i+1} = x{i}^2 mod n 30 | # is equal to n-1 31 | (miller-rabin-loop ?r ?x ?n) = ( 32 | iter (?x => mod %n (pow 2 %x)) %x | take %r | any [eq (dec %n)] 33 | ) 34 | 35 | # Generate a random number in the range [begin, end] 36 | (random-number-in-range ?begin ?end) = ( 37 | mul (sub %begin %end) (rand) | into round 0 | into add %begin 38 | ) 39 | 40 | # Helper method for the Miller-rabin primality test 41 | # Checks if num is prime using i random numbers 42 | # r and d are found from (n-1) = 2^r * d 43 | (miller-rabin-test ?i ?num ?r ?d) = ( 44 | range %i | all (_ => 45 | rnum = (random-number-in-range 2 (sub 2 %num)) 46 | x = (pow %d %rnum | into mod %num) 47 | eq 1 %x || eq %x (dec %num) || (miller-rabin-loop %r %x %num) 48 | ) 49 | ) 50 | 51 | # The Miller-rabin primality test 52 | # Succeeds if num is probably prime. 53 | # You can increase i to increase the certainty. 54 | # Fails if num is not prime 55 | (miller-rabin ?i ?num) = ( 56 | eq 1 (mod 2 %num) && 57 | (r d = (miller-rabin-factoring (dec %num) 0) 58 | (miller-rabin-test %i %num %r %d)) 59 | ) 60 | 61 | (repeat-func ?fn) = (put (exec %fn); repeat-func %fn) 62 | 63 | (gen-prime ?b) = ( 64 | repeat-func [gen-number %b] | each (?n => iter (?x => inc %x) %n | take %b) | 65 | filter [miller-rabin 10] | take 1 66 | ) 67 | 68 | put (gen-prime 5) 69 | -------------------------------------------------------------------------------- /lib/mag/prelude.mag: -------------------------------------------------------------------------------- 1 | @allow-intrinsics 2 | 3 | __repr__ = "{ }" 4 | 5 | (put ...?a) = @!for $a 6 | (get) = @!get 7 | (take ?n) = @!take $n 8 | 9 | add = @!add 10 | mul = @!mul 11 | str = @!str 12 | 13 | (puts ...?a) = put (str ...$a) 14 | 15 | (drain) = (@!get; drain) 16 | (repeat ...?a) = (@!for $a; repeat ...$a) 17 | (iter ?fn ?x) = (put $x; iter $fn ($fn $x)) 18 | (each ...?fn) = ( 19 | (k) = (%fn (@!get); k) 20 | k 21 | ) 22 | 23 | for = @!for 24 | 25 | (crash ?msg) = @!crash $msg 26 | 27 | exec = [] 28 | 29 | (true) = () 30 | (false) = (@!fail false) 31 | 32 | (make-channel) = @!make-channel 33 | 34 | (count-forever) = iter [@!add 1] 0 35 | 36 | (eq ?x ?y) = @!eq $x $y 37 | 38 | (len ?v) = @!len $v 39 | 40 | (getenv ?s) = @!getenv $s 41 | 42 | vm-debug = ( 43 | on => @!vm-debug 10 44 | off => @!vm-debug -1 45 | ?level => @!vm-debug $level 46 | open ?fname => @!vm-debug-open $fname; @!vm-debug 10 47 | ) 48 | 49 | LOADED_MODULES = {} 50 | 51 | (require ?fname) = ( 52 | @!has $fname $LOADED_MODULES || ( 53 | $LOADED_MODULES!$fname = 1 54 | load $fname 55 | ) 56 | ) 57 | 58 | (label ?l) = (each (?x => put [%l %x])) 59 | 60 | (filter ?fn) = each (?x => %fn $x && put $x) 61 | 62 | # # 63 | # # (times ?n ?fn (?args)) = ( 64 | # # in = (stdin) 65 | # # range %n | each (_ => exec %fn (for %args) < %in) 66 | # # ) 67 | # # 68 | # # (fan ?nthreads ?fn (?args)) = ( 69 | # # times %nthreads (=> 70 | # # & each [%fn (for %args)] 71 | # # ) 72 | # # ) 73 | # # 74 | # # (range ?n) = (count-forever | take %n) 75 | # # (repeat-forever ?val) = (put %val; repeat-forever %val) 76 | # # (repeat ?n ?val) = (repeat-forever %val | take %n) 77 | # # (inc ?val) = (add %val 1) 78 | # # (dec ?val) = (add %val -1) 79 | # # 80 | # # (into ?f (?a)) = (%f (for %a) (drain)) 81 | # # 82 | # # 83 | # # (prob ?total ?amt) = (lt %amt (mul %total (rand))) 84 | # # 85 | # # (sample) = ( 86 | # # hold = (get) 87 | # # i = 1 88 | # # each (?v => 89 | # # # local 90 | # # %i = (inc %i) 91 | # # prob %i 1 && (%hold = %v) 92 | # # ) 93 | # # 94 | # # put %hold 95 | # # ) 96 | # # 97 | # # (produce ?fn) = (loop-channel (stdout) %fn) 98 | # # (consume ?fn) = (loop-channel (stdin) %fn) 99 | # # 100 | # # (iter ?fn ?a) = (produce (=> (put %a; %a = (%fn %a)))) 101 | # # (each ?fn) = (consume (=> %fn (get))) 102 | # # 103 | # # (even ?x) = (eq 0 (mod 2 %x)) 104 | # # (odd ?x) = (eq 1 (mod 2 %x)) 105 | # # (not (?args)) = (exec (for %args) && false !! true) 106 | # # (all ?pred) = (not any [not %pred]) 107 | # # 108 | # # (take-until ?pred) = (each (?e => %pred %e && put %e !! false)) 109 | # # 110 | # # stdout = @!stdout 111 | # # LOG = (stdout) 112 | # # (log ?msg) = (put ["log:" %msg] > $LOG) 113 | # # 114 | # # null = (make-null) 115 | # # (through ?o) = ( 116 | # # & %o!from < %null 117 | # # & %o!into > %null 118 | # # ) 119 | -------------------------------------------------------------------------------- /lib/magc/ast.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | module AST 3 | 4 | class Variable < Tree::Node 5 | defdata :name 6 | end 7 | 8 | class LexVariable < Tree::Node 9 | defdata :name 10 | end 11 | 12 | class Intrinsic < Tree::Node 13 | defdata :name 14 | end 15 | 16 | class String < Tree::Node 17 | defdata :value 18 | end 19 | 20 | class Number < Tree::Node 21 | defdata :value 22 | end 23 | 24 | class Binder < Tree::Node 25 | defdata :name 26 | end 27 | 28 | class VariablePattern < Tree::Node 29 | defrec :var 30 | end 31 | 32 | class StringPattern < Tree::Node 33 | defdata :value 34 | end 35 | 36 | class VectorPattern < Tree::Node 37 | deflistrec :patterns 38 | defopt :rest 39 | end 40 | 41 | class DefaultPattern < Tree::Node 42 | end 43 | 44 | class RestPattern < Tree::Node 45 | defrec :binder 46 | end 47 | 48 | class Lambda < Tree::Node 49 | defdata :name 50 | deflistrec :patterns 51 | deflistrec :bodies 52 | defdata :range 53 | 54 | def initialize(*) 55 | super 56 | raise "Pattern and body mismatch" unless patterns.size == bodies.size 57 | end 58 | end 59 | 60 | class Pipe < Tree::Node 61 | defrec :producer 62 | defrec :consumer 63 | end 64 | 65 | class Or < Tree::Node 66 | defrec :lhs 67 | defrec :rhs 68 | 69 | def continue?(status) 70 | status.fail? 71 | end 72 | end 73 | 74 | class And < Tree::Node 75 | defrec :lhs 76 | defrec :rhs 77 | 78 | def continue?(status) 79 | status.normal? 80 | end 81 | end 82 | 83 | class Else < Tree::Node 84 | defrec :lhs 85 | defrec :rhs 86 | end 87 | 88 | class Compensation < Tree::Node 89 | defrec :expr 90 | defrec :compensation 91 | defdata :range 92 | defdata :unconditional 93 | end 94 | 95 | class Spawn < Tree::Node 96 | defrec :expr 97 | end 98 | 99 | class Redirect < Tree::Node 100 | defdata :direction 101 | defrec :target 102 | end 103 | 104 | class With < Tree::Node 105 | deflistrec :redirects 106 | defrec :expr 107 | end 108 | 109 | class Command < Tree::Node 110 | deflistrec :vec 111 | defdata :range 112 | 113 | def initialize(*) 114 | super 115 | raise "Empty command" unless vec.any? 116 | end 117 | end 118 | 119 | class Block < Tree::Node 120 | defrec :group 121 | end 122 | 123 | class Group < Tree::Node 124 | deflistrec :elems 125 | end 126 | 127 | class Subst < Tree::Node 128 | defrec :group 129 | end 130 | 131 | class Vector < Tree::Node 132 | deflistrec :elems 133 | end 134 | 135 | class Environment < Tree::Node 136 | defrec :body 137 | end 138 | 139 | class Access < Tree::Node 140 | defrec :source 141 | defrec :lookup 142 | end 143 | 144 | class Splat < Tree::Node 145 | defrec :expr 146 | end 147 | 148 | class Assignment < Tree::Node 149 | deflistrec :lhs 150 | deflistrec :rhs 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/magc/cli.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | class CLI 3 | def self.run(argv) 4 | new(argv).run 5 | end 6 | 7 | def initialize(argv) 8 | @argv = argv 9 | end 10 | 11 | def compile_files(files) 12 | files.each do |file| 13 | ast = Parser.parse(Skeleton.parse(Lexer.new(file, File.read(file)))) 14 | 15 | compiler = Magritte::Compiler.new(ast) 16 | compiler.compile 17 | compiler.render_decomp(File.open("#{file}x", 'w')) 18 | compiler.render(File.open("#{file}c", 'w')) 19 | end 20 | end 21 | 22 | def run 23 | compile_files(@argv) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/magc/env.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | class EnvRef 3 | attr_accessor :value 4 | def initialize(value) 5 | @value = value 6 | end 7 | end 8 | 9 | class Env 10 | class MissingVariable < StandardError 11 | def initialize(name, env) 12 | @name = name 13 | @env = env 14 | end 15 | 16 | def to_s 17 | "No such variable: #{@name}" 18 | end 19 | end 20 | 21 | class << self 22 | KEY = :__MAGRITTE_ENV__ 23 | 24 | def current 25 | Thread.current[KEY] ||= new 26 | end 27 | 28 | def with(bindings={}) 29 | old = self.current 30 | Thread.current[KEY] = old.with(bindings) 31 | ensure 32 | Thread.current[KEY] = old 33 | end 34 | end 35 | 36 | attr_reader :keys, :own_inputs, :own_outputs 37 | def initialize(parent=nil, keys={}, inputs=[], outputs=[]) 38 | @parent = parent 39 | @keys = keys 40 | @own_inputs = inputs 41 | @own_outputs = outputs 42 | end 43 | 44 | def get_parent 45 | @parent 46 | end 47 | 48 | def input(n) 49 | @own_inputs[n] or parent :input, n 50 | end 51 | 52 | def output(n) 53 | @own_outputs[n] or parent :output, n 54 | end 55 | 56 | def set_input(n, ch) 57 | @own_inputs[n] = ch 58 | end 59 | 60 | def set_output(n, ch) 61 | @own_outputs[n] = ch 62 | end 63 | 64 | def each_input 65 | (0..32).each do |x| 66 | ch = input(x) or return 67 | yield ch 68 | end 69 | end 70 | 71 | def each_output 72 | (0..32).each do |x| 73 | ch = output(x) or return 74 | yield ch 75 | end 76 | end 77 | 78 | def stdin 79 | input(0) 80 | end 81 | 82 | def stdout 83 | output(0) 84 | end 85 | 86 | def splice(new_parent) 87 | Env.new(new_parent, @keys.dup, @own_inputs.dup, @own_outputs.dup) 88 | end 89 | 90 | def slice(keys) 91 | refs = {} 92 | keys.each do |key| 93 | refs[normalize_key(key)] = ref(key) 94 | end 95 | Env.new(nil, refs, [], []) 96 | end 97 | 98 | def self.empty 99 | new(nil, {}, [], []) 100 | end 101 | 102 | def self.base 103 | env = Env.empty 104 | Builtins.load(env) 105 | end 106 | 107 | def extend(inputs=[], outputs=[]) 108 | self.class.new(self, {}, inputs, outputs) 109 | end 110 | 111 | def key?(key) 112 | ref(key) 113 | true 114 | rescue MissingVariable 115 | false 116 | end 117 | 118 | def mut(key, val) 119 | ref(key).value = val 120 | end 121 | 122 | def let(key, val) 123 | @keys[normalize_key(key)] = EnvRef.new(val) 124 | self 125 | end 126 | 127 | def get(key) 128 | ref(key).value 129 | end 130 | 131 | def unhinge! 132 | @parent = nil 133 | end 134 | 135 | def repr 136 | visited_envs = [] 137 | out = "" 138 | curr_env = self 139 | nest_counter = 1 140 | while !curr_env.nil? 141 | if visited_envs.include? curr_env 142 | out << "" 143 | break 144 | elsif curr_env.own_key?('__repr__') 145 | out << curr_env.get('__repr__').value 146 | break 147 | end 148 | 149 | visited_envs << curr_env 150 | 151 | out << "{" 152 | 153 | env_content = [] 154 | env_content.concat (curr_env.keys.map { |key| " #{key.first} = #{key[1].value.repr}" }) 155 | env_content.concat (curr_env.own_inputs.map { |input| " < #{input.repr}" }) 156 | env_content.concat (curr_env.own_outputs.map { |output| " > #{output.repr}" }) 157 | 158 | out << env_content.join(";") 159 | 160 | if !curr_env.get_parent.nil? 161 | out << " + " 162 | nest_counter += 1 163 | end 164 | curr_env = curr_env.get_parent 165 | end 166 | while nest_counter > 0 167 | out << " }" 168 | nest_counter -= 1 169 | end 170 | out 171 | end 172 | 173 | def inspect 174 | "#" 175 | end 176 | 177 | protected 178 | def own_key?(key) 179 | @keys.key?(key.to_s) 180 | end 181 | 182 | def own_ref(key) 183 | @keys[key] 184 | end 185 | 186 | def normalize_key(key) 187 | key.to_s 188 | end 189 | 190 | def ref(key) 191 | key = normalize_key(key) 192 | 193 | if own_key?(key) 194 | @keys[key] 195 | elsif @parent 196 | @parent.ref(key) 197 | else 198 | missing!(key) 199 | end 200 | end 201 | 202 | def missing!(name) 203 | raise MissingVariable.new(name, self) 204 | end 205 | 206 | private 207 | def parent(method, *a, &b) 208 | @parent && @parent.__send__(method, *a, &b) 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/magc/free_vars.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | module FreeVars 3 | def self.scan(node) 4 | Scanner.new.collect(node, Set.new) 5 | end 6 | 7 | class BinderScanner < Tree::Collector 8 | def visit_binder(node) 9 | Set.new([node.name]) 10 | end 11 | end 12 | 13 | class Scanner < Tree::Collector 14 | def visit_lex_variable(node, bound_vars) 15 | Set.new([node.name]) 16 | end 17 | 18 | def visit_lambda(node, bound_vars) 19 | out = Set.new 20 | node.patterns.zip(node.bodies) do |pat, body| 21 | binders = BinderScanner.new.collect_one(pat) 22 | out.merge(shadow(body, bound_vars, binders)) 23 | end 24 | out 25 | end 26 | 27 | def visit_group(node, bound_vars) 28 | out = Set.new 29 | so_far = Set.new 30 | node.elems.each do |elem| 31 | case elem 32 | when AST::Assignment 33 | recursive = so_far.dup 34 | elem.lhs.each do |binder| 35 | recursive << binder.value if binder.is_a?(AST::String) 36 | out.merge(shadow(binder, bound_vars, so_far)) 37 | end 38 | 39 | elem.rhs.each do |el| 40 | out.merge(shadow(el, bound_vars, el.is_a?(AST::Lambda) ? recursive : so_far)) 41 | end 42 | 43 | so_far = recursive 44 | else 45 | out.merge(shadow(elem, bound_vars, so_far)) 46 | end 47 | end 48 | 49 | out 50 | end 51 | 52 | def shadow(node, bound_vars, shadow_vars) 53 | visit(node, bound_vars + shadow_vars) - shadow_vars 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/magc/index.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module Magritte 4 | def self.reload! 5 | Object.send(:remove_const, :Magritte) 6 | load __FILE__ 7 | self 8 | end 9 | 10 | class CompileError < StandardError 11 | end 12 | 13 | LIB_DIR = File.dirname(__FILE__) 14 | 15 | load "#{LIB_DIR}/log.rb" 16 | 17 | # AST 18 | load "#{LIB_DIR}/tree.rb" 19 | load "#{LIB_DIR}/ast.rb" 20 | load "#{LIB_DIR}/free_vars.rb" 21 | 22 | # Lexer/Parser 23 | load "#{LIB_DIR}/lexer.rb" 24 | load "#{LIB_DIR}/skeleton.rb" 25 | load "#{LIB_DIR}/matcher.rb" 26 | load "#{LIB_DIR}/parser.rb" 27 | 28 | # Compiler 29 | load "#{LIB_DIR}/value.rb" 30 | load "#{LIB_DIR}/compiler.rb" 31 | 32 | load "#{LIB_DIR}/cli.rb" 33 | end 34 | -------------------------------------------------------------------------------- /lib/magc/lexer.rb: -------------------------------------------------------------------------------- 1 | require 'strscan' 2 | require 'set' 3 | module Magritte 4 | class Lexer 5 | 6 | class LexError < CompileError 7 | def initialize(location, msg) 8 | @location = location 9 | @msg = msg 10 | end 11 | 12 | attr_reader :location 13 | attr_reader :msg 14 | 15 | def to_s 16 | "Lexing Error: #{@msg} at #{@location.repr}" 17 | end 18 | end 19 | 20 | class Token 21 | attr_reader :type 22 | attr_reader :value 23 | attr_reader :range 24 | NESTED_PAIRS = { 25 | :lparen => :rparen, 26 | :lbrack => :rbrack, 27 | :lbrace => :rbrace, 28 | } 29 | CONTINUE = Set.new([ 30 | :pipe, 31 | :write_to, 32 | :read_from, 33 | :d_amp, 34 | :d_bar, 35 | :d_per, 36 | :d_bang, 37 | :d_per_bang, 38 | :arrow, 39 | ]) 40 | 41 | FREE_NL = Set.new([ 42 | :lbrack, 43 | ]) 44 | 45 | def initialize(type, value, range) 46 | @type = type 47 | @value = value 48 | @range = range 49 | end 50 | 51 | def continue? 52 | CONTINUE.include?(@type) 53 | end 54 | 55 | def nest? 56 | NESTED_PAIRS.key?(@type) 57 | end 58 | 59 | def nest_pair 60 | NESTED_PAIRS[@type] 61 | end 62 | 63 | def free_nl? 64 | FREE_NL.include?(@type) 65 | end 66 | 67 | def is?(type) 68 | @type == type 69 | end 70 | 71 | def eof? 72 | @type == :eof 73 | end 74 | 75 | def repr 76 | if @value.nil? 77 | "#{@type}" 78 | else 79 | "#{@type}/#{@value}" 80 | end 81 | end 82 | 83 | # Object equality based on 84 | # https://stackoverflow.com/questions/1931604/whats-the-right-way-to-implement-equality-in-ruby 85 | def ==(o) 86 | o.class == self.class && o.type == @type && o.value == @value 87 | end 88 | 89 | def inspect 90 | "#<#{self.class.name} #{self.repr}>" 91 | end 92 | end 93 | 94 | include Enumerable 95 | 96 | def initialize(source_name, string) 97 | @source_name = source_name 98 | @scanner = StringScanner.new(string) 99 | @line = 1 100 | @col = 0 101 | skip_lines 102 | end 103 | 104 | attr_reader :source_name 105 | 106 | def peek 107 | @peek ||= self.next 108 | end 109 | 110 | WORD = /[\w-]+/ 111 | 112 | def next 113 | if @peek 114 | p = @peek 115 | @peek = nil 116 | return p 117 | end 118 | 119 | if @scanner.eos? 120 | return token(:eof) 121 | elsif scan /[\n;]|(#[^\n]*)/ 122 | skip_lines 123 | return token(:nl) 124 | elsif scan /[(]/ 125 | skip_lines 126 | return token(:lparen) 127 | elsif scan /[)]/ 128 | skip_ws 129 | return token(:rparen) 130 | elsif scan /[{]/ 131 | skip_lines 132 | return token(:lbrace) 133 | elsif scan /[}]/ 134 | skip_ws 135 | return token(:rbrace) 136 | elsif scan /\[/ 137 | skip_lines 138 | return token(:lbrack) 139 | elsif scan /\]/ 140 | skip_ws 141 | return token(:rbrack) 142 | elsif scan /\=>/ 143 | skip_ws 144 | return token(:arrow) 145 | elsif scan /[=]/ 146 | skip_ws 147 | return token(:equal) 148 | elsif scan // 152 | skip_ws 153 | return token(:gt) 154 | elsif scan /%%!/ 155 | skip_ws 156 | return token(:d_per_bang) 157 | elsif scan /%%/ 158 | skip_ws 159 | return token(:d_per) 160 | elsif scan /!!/ 161 | skip_ws 162 | return token(:d_bang) 163 | elsif scan /&&/ 164 | skip_ws 165 | return token(:d_amp) 166 | elsif scan /\|\|/ 167 | skip_ws 168 | return token(:d_bar) 169 | elsif scan /&/ 170 | skip_ws 171 | return token(:amp) 172 | elsif scan /\|/ 173 | skip_ws 174 | return token(:pipe) 175 | elsif scan /!/ 176 | skip_ws 177 | return token(:bang) 178 | elsif scan /[.][.][.]/ 179 | skip_lines 180 | return token(:ellipsis) 181 | elsif scan /[$](#{WORD})/ 182 | skip_ws 183 | return token(:var, group(1)) 184 | elsif scan /[%](#{WORD})/ 185 | skip_ws 186 | return token(:lex_var, group(1)) 187 | elsif scan /[?](#{WORD})/ 188 | skip_ws 189 | return token(:bind, group(1)) 190 | elsif scan /[@]!(#{WORD})/ 191 | skip_ws 192 | return token(:intrinsic, group(1)) 193 | elsif scan /[@](#{WORD})/ 194 | skip_ws 195 | return token(:keyword, group(1)) 196 | elsif scan /([-]?[0-9]+([\.][0-9]*)?)/ 197 | skip_ws 198 | return token(:num, group(1)) 199 | elsif scan /"((?:\\.|[^"])*)"/ 200 | skip_ws 201 | return token(:string, group(1)) 202 | elsif scan /'(.*?)'/m 203 | skip_ws 204 | return token(:string, group(1)) 205 | elsif scan /([:_.\/a-zA-Z0-9-]+)/ 206 | skip_ws 207 | return token(:bare, group(1)) 208 | else 209 | error!("Unknown token near #{@scanner.peek(3)}") 210 | end 211 | end 212 | 213 | class Location 214 | include Comparable 215 | 216 | def range 217 | Range.new(self, self) 218 | end 219 | 220 | def initialize(source_name, source, line, col, index) 221 | @source_name = source_name 222 | @source = source 223 | @line = line 224 | @col = col 225 | @index = index 226 | end 227 | 228 | attr_reader :source_name 229 | attr_reader :source 230 | attr_reader :line 231 | attr_reader :col 232 | attr_reader :index 233 | 234 | def <=>(other) 235 | raise "Incomparable" unless source_name == other.source_name 236 | self.index <=> other.index 237 | end 238 | 239 | def repr 240 | "#{line}:#{col}" 241 | end 242 | end 243 | 244 | class Range 245 | 246 | def range 247 | self 248 | end 249 | 250 | def self.between(start, fin) 251 | start_loc = start.range.first 252 | fin_loc = fin.range.last 253 | if start_loc.source_name != fin_loc.source_name 254 | raise "Can't compute Range.between, mismatching source names: #{start_loc.source_name} != #{fin_loc.source_name}" 255 | end 256 | 257 | new(start_loc, fin_loc) 258 | end 259 | 260 | def initialize(first, last) 261 | @first = first 262 | @last = last 263 | end 264 | 265 | attr_reader :first 266 | attr_reader :last 267 | 268 | def repr 269 | "#{first.source_name}@#{first.repr}~#{last.repr}" 270 | end 271 | end 272 | 273 | # This function is called when we have instantiated a lexer 274 | # and want to generate tokens of the entire program 275 | # This is for example used in the lexer_spec 276 | # when we call lex.to_a to generate the array 277 | # of tokens 278 | def lex(&block) 279 | loop do 280 | token = self.next 281 | yield token 282 | break if token.eof? 283 | end 284 | end 285 | alias each lex 286 | 287 | private 288 | def scan(re) 289 | prev_pos = current_pos 290 | if @scanner.scan(re) 291 | @match = @scanner.matched 292 | @groups = @scanner.captures 293 | update_line_col(@match) 294 | @last_range = Range.new(prev_pos, current_pos) 295 | true 296 | else 297 | @match = nil 298 | @groups = [] 299 | @last_range = [0, 0] # Is this really correct? 300 | false 301 | end 302 | end 303 | 304 | def current_pos 305 | Location.new(@source_name, @scanner.string, @line, @col, @scanner.pos) 306 | end 307 | 308 | def update_line_col(string) 309 | nlcount = string.scan(/\n/).size 310 | @line += nlcount 311 | if nlcount > 0 312 | string =~ /\n.*?\z/ 313 | @col = $&.size 314 | else 315 | @col += string.size 316 | end 317 | end 318 | 319 | def match 320 | @match 321 | end 322 | 323 | def group(index) 324 | @groups[index-1] 325 | end 326 | 327 | def token(type, value = nil) 328 | Token.new(type, value, @last_range) 329 | end 330 | 331 | def skip_ws 332 | skip(/[ \t]+/) 333 | end 334 | 335 | def skip_lines 336 | skip /((#[^\n]*)?\n[ \t;]*)*[ \t;]*/m 337 | end 338 | 339 | def skip(re) 340 | @scanner.skip(re) and update_line_col(@scanner.matched) 341 | end 342 | 343 | def error!(msg) 344 | raise LexError.new(current_pos, msg) 345 | end 346 | end 347 | end 348 | -------------------------------------------------------------------------------- /lib/magc/log.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | class Printer 3 | def initialize 4 | @mutex = Mutex.new 5 | end 6 | 7 | def puts(*a) 8 | @mutex.synchronize { ::Kernel.puts(*a) } 9 | end 10 | 11 | def p(*a) 12 | @mutex.synchronize { ::Kernel.p(*a) } 13 | end 14 | end 15 | 16 | class NullPrinter < Printer 17 | def puts(*); end 18 | def p(*); end 19 | end 20 | 21 | class LogPrinter 22 | def self.thread_name(t) 23 | t.inspect =~ /0x\h+/ 24 | $& 25 | end 26 | 27 | def initialize(prefix) 28 | @prefix = prefix 29 | end 30 | 31 | def current_log 32 | Thread.current[:magritte_log] ||= [] 33 | end 34 | 35 | def fname 36 | LogPrinter.thread_name(Thread.current) 37 | end 38 | 39 | def with_file(&b) 40 | File.open("#{@prefix}/#{fname}", 'a', &b) 41 | end 42 | 43 | def p(*a) 44 | with_file { |f| f << a.map(&:inspect).join(' / ') << "\n" } 45 | a.each { |l| current_log << l } 46 | end 47 | 48 | def puts(*a) 49 | with_file { |f| f.puts(*a) } 50 | a.each { |l| current_log << l } 51 | end 52 | end 53 | 54 | PRINTER = case ENV['MAGRITTE_DEBUG'] 55 | when nil, "" 56 | NullPrinter.new 57 | when 'log' 58 | Dir.mkdir("./tmp/log/#{$$}") 59 | LogPrinter.new("./tmp/log/#{$$}") 60 | else 61 | Printer.new 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/magc/matcher.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | module Matcher 3 | class MatchFail < StandardError 4 | end 5 | 6 | class Base < Tree::Node 7 | def match_vars(skel) 8 | return enum_for(:test, skel).to_a 9 | rescue MatchFail 10 | nil 11 | end 12 | 13 | def test(skel,&b) 14 | raise "Not implemented!" 15 | end 16 | 17 | def ~@ 18 | Capture[self] 19 | end 20 | 21 | protected 22 | def fail! 23 | raise MatchFail.new 24 | end 25 | 26 | def matches?(skel, &b) 27 | out = [] 28 | test(skel) { |x| out << x } 29 | rescue MatchFail 30 | return false 31 | else 32 | out.each { |x| b.call x } 33 | true 34 | end 35 | end 36 | 37 | class Ignore < Base 38 | def test(skel, &b) 39 | end 40 | end 41 | 42 | class Empty < Base 43 | def test(skel, &b) 44 | fail! unless skel.is_a? Skeleton::Item 45 | fail! unless skel.elems.empty? 46 | end 47 | end 48 | 49 | class TokenType < Base 50 | defdata :type 51 | 52 | def test(skel, &b) 53 | fail! unless skel.is_a? Skeleton::Token 54 | fail! unless skel.token.is? type 55 | end 56 | end 57 | 58 | class Item < Base 59 | def test(skel, &b) 60 | fail! unless skel.is_a? Skeleton::Item 61 | end 62 | end 63 | 64 | class Singleton < Base 65 | defrec :matcher 66 | 67 | def test(skel, &b) 68 | fail! unless skel.is_a? Skeleton::Item 69 | fail! unless skel.elems.size == 1 70 | fail! unless matcher.matches?(skel.elems.first, &b) 71 | end 72 | end 73 | 74 | class Capture < Base 75 | defrec :matcher 76 | 77 | def test(skel, &b) 78 | fail! unless matcher.matches?(skel, &b) 79 | yield skel 80 | end 81 | end 82 | 83 | class Nested < Base 84 | defdata :open_type 85 | defrec :matcher 86 | 87 | def test(skel, &b) 88 | fail! unless skel.is_a?(Skeleton::Nested) 89 | fail! unless skel.open.is?(open_type) 90 | fail! unless matcher.matches?(Skeleton::Item[skel.elems], &b) # Why do we wrap the elems in an Item? 91 | end 92 | end 93 | 94 | class LSplit < Base 95 | defrec :before 96 | defrec :split 97 | defrec :after 98 | 99 | def test(skel, &block) 100 | fail! unless skel.is_a? Skeleton::Item 101 | matched = false 102 | before = [] 103 | after = [] 104 | skel.elems.each do |elem| 105 | next after << elem if matched 106 | next before << elem unless self.split.matches?(elem, &block) 107 | matched = true 108 | end 109 | fail! unless matched 110 | fail! unless self.before.matches?(Skeleton::Item[before], &block) 111 | fail! unless self.after.matches?(Skeleton::Item[after], &block) 112 | end 113 | end 114 | 115 | class RSplit < Base 116 | defrec :before 117 | defrec :split 118 | defrec :after 119 | 120 | def test(skel, &block) 121 | fail! unless skel.is_a? Skeleton::Item 122 | matched = false 123 | before = [] 124 | after = [] 125 | skel.elems.reverse_each do |elem| 126 | next before << elem if matched 127 | next after << elem unless self.split.matches?(elem, &block) 128 | matched = true 129 | end 130 | fail! unless matched 131 | fail! unless self.before.matches?(Skeleton::Item[before.reverse], &block) 132 | fail! unless self.after.matches?(Skeleton::Item[after.reverse], &block) 133 | end 134 | end 135 | 136 | class All < Base 137 | defrec :matcher 138 | 139 | def test(skel, &b) 140 | fail! unless skel.is_a? Skeleton::Item 141 | skel.elems.each do |elem| 142 | fail! unless self.matcher.matches?(elem, &b) 143 | end 144 | end 145 | end 146 | 147 | class Any < Base 148 | deflistrec :matchers 149 | 150 | def test(skel, &b) 151 | matchers.each do |matcher| 152 | return if matcher.matches?(skel, &b) 153 | end 154 | fail! 155 | end 156 | end 157 | 158 | class Nay < Base 159 | defrec :matcher 160 | 161 | def test(skel, &b) 162 | fail! unless skel.is_a? Skeleton::Item 163 | skel.elems.each do |elem| 164 | fail! if self.matcher.matches?(elem, &b) 165 | end 166 | fail! if self.matcher.matches?(skel, &b) 167 | end 168 | end 169 | 170 | module DSL 171 | def token(type) 172 | TokenType[type] 173 | end 174 | 175 | def nested(type, matcher) 176 | Nested[type, matcher] 177 | end 178 | 179 | def singleton(matcher) 180 | Singleton[matcher] 181 | end 182 | 183 | def starts(elem, rest) 184 | lsplit(empty, elem, rest) 185 | end 186 | 187 | def ends(elem, rest) 188 | rsplit(rest, elem, empty) 189 | end 190 | 191 | def lsplit(before, split, after) 192 | LSplit[before, split, after] 193 | end 194 | 195 | def rsplit(before, split, after) 196 | RSplit[before, split, after] 197 | end 198 | 199 | def any(*args) 200 | Any[args] 201 | end 202 | 203 | def all(arg) 204 | All[arg] 205 | end 206 | 207 | def nay(arg) 208 | Nay[arg] 209 | end 210 | 211 | def _ 212 | Ignore[] 213 | end 214 | 215 | def empty 216 | Empty[] 217 | end 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /lib/magc/skeleton.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | module Skeleton 3 | def self.parse(*args) 4 | Parser.parse(*args) 5 | end 6 | 7 | class Base < Tree::Node 8 | def inspect 9 | "#" 10 | end 11 | 12 | def match(matcher, &b) 13 | vars = matcher.match_vars(self) 14 | return false if vars.nil? 15 | yield(*vars) if block_given? 16 | true 17 | end 18 | 19 | def ==(other) 20 | return false if self.class != other.class 21 | same?(other) 22 | end 23 | 24 | def token?(type = nil) 25 | false 26 | end 27 | 28 | def nested?(type = nil) 29 | false 30 | end 31 | 32 | def range 33 | raise "Abstract" 34 | end 35 | 36 | private 37 | def all_eq?(as, bs) 38 | as.zip(bs) { |a, b| return false unless a == b } 39 | true 40 | end 41 | end 42 | 43 | class Token < Base 44 | defdata :token 45 | 46 | def repr 47 | ".#{token.repr}" 48 | end 49 | 50 | def same?(other) 51 | token.type == other.token.type && token.value == other.token.value 52 | end 53 | 54 | def token?(type = nil) 55 | if type.nil? 56 | return true 57 | else 58 | return token.is?(type) 59 | end 60 | end 61 | 62 | def value 63 | token.value 64 | end 65 | 66 | def range 67 | token.range 68 | end 69 | end 70 | 71 | class Nested < Base 72 | defdata :open 73 | defdata :close 74 | deflistrec :elems 75 | 76 | def same?(other) 77 | return false unless open == other.open 78 | return false unless close == other.close 79 | return false unless elems.size == other.elems.size 80 | return false unless all_eq?(elems, other.elems) 81 | 82 | true 83 | end 84 | 85 | def repr 86 | "[#{open.repr}|#{elems.map(&:repr).join(" ")}|#{close.repr}]" 87 | end 88 | 89 | def nested?(type = nil) 90 | return true unless type 91 | open.is?(type) 92 | end 93 | 94 | def range 95 | Lexer::Range.between(open, close) 96 | end 97 | end 98 | 99 | class Item < Base 100 | defdata :elems # Why isn't this one deflistrec? 101 | 102 | def same?(other) 103 | return false unless elems.size == other.elems.size 104 | return false unless all_eq?(elems, other.elems) 105 | 106 | true 107 | end 108 | 109 | def sub_items 110 | return self.elems if self.elems.all? { |e| e.is_a?(Item) } 111 | [self] 112 | end 113 | 114 | def repr 115 | "(#{elems.map(&:repr).join(" ")})" 116 | end 117 | 118 | def range 119 | Lexer::Range.between(elems.first, elems.last) 120 | end 121 | end 122 | 123 | class Root < Base 124 | defdata :elems # Same question as for Item class 125 | 126 | def same?(other) 127 | all_eq?(elems, other.elems) 128 | end 129 | 130 | def sub_items 131 | elems 132 | end 133 | 134 | def repr 135 | "(#{elems.map(&:repr).join(" ")})" 136 | end 137 | 138 | def range 139 | Lexer::Range.between(elems.first, elems.last) 140 | end 141 | end 142 | 143 | class NestingError < CompileError 144 | def initialize(open, close, type) 145 | @open = open 146 | @close = close 147 | @type = type 148 | end 149 | 150 | def to_s 151 | "#{@type} at #{Lexer::Range.between(@open, @close).repr}" 152 | end 153 | end 154 | 155 | class Parser 156 | def self.parse(lexer) 157 | new(nil, :eof).parse(lexer) 158 | end 159 | 160 | attr_reader :open 161 | attr_reader :expected_close 162 | 163 | def initialize(open, expected_close) 164 | @open = open 165 | @expected_close = expected_close 166 | @items = [[]] 167 | end 168 | 169 | def parse(lexer) 170 | last = nil 171 | token = nil 172 | 173 | loop do 174 | last = token 175 | token = lexer.next 176 | if token.eof? and @open.nil? 177 | return Root[items] 178 | elsif token.eof? 179 | error!(token, "Unmatched nesting") 180 | elsif token.is?(expected_close) 181 | return Nested[self.open, token, out] 182 | elsif token.nest? 183 | y Parser.new(token, token.nest_pair).parse(lexer) 184 | elsif token.is?(:nl) 185 | # As we instantiate the parser with open = nil 186 | # it can happen that we try to call free_nl? 187 | # on a nil object.... 188 | next if !self.open.nil? && self.open.free_nl? 189 | next if (last && last.continue?) || lexer.peek.continue? 190 | @items << [] 191 | else 192 | y Token[token] 193 | end 194 | end 195 | end 196 | 197 | private 198 | def error!(token, type) 199 | raise NestingError.new(@open, token, type) 200 | end 201 | 202 | def y(node) 203 | @items.last << node 204 | end 205 | 206 | def items 207 | @items.select(&:any?).map { |nodes| Item[nodes] } 208 | end 209 | 210 | def out 211 | if @items.size == 1 212 | @items[0] 213 | else 214 | items 215 | end 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/magc/tree.rb: -------------------------------------------------------------------------------- 1 | module Magritte 2 | module Tree 3 | class Attr 4 | attr_reader :value 5 | def initialize(value) 6 | @value = value 7 | end 8 | 9 | def each(&block) 10 | raise "abstract" 11 | end 12 | 13 | def map(&block) 14 | raise "abstract" 15 | end 16 | 17 | # Object equality based on 18 | # https://stackoverflow.com/questions/1931604/whats-the-right-way-to-implement-equality-in-ruby 19 | def ==(o) 20 | o.class == self.class && o.value == @value 21 | end 22 | 23 | alias eql? == 24 | 25 | def hash 26 | [self.class, self.value].hash 27 | end 28 | 29 | def full_repr(depth) 30 | inspect 31 | end 32 | 33 | def repr(depth) 34 | return '...' if depth.zero? 35 | full_repr(depth-1) 36 | end 37 | end 38 | 39 | class DataAttr < Attr 40 | def each(&block) 41 | #pass 42 | end 43 | 44 | def map(&block) 45 | self 46 | end 47 | 48 | def inspect 49 | value.inspect 50 | end 51 | 52 | def full_repr(depth) 53 | value.repr 54 | rescue 55 | value.inspect 56 | end 57 | end 58 | 59 | class RecAttr < Attr 60 | def each(&block) 61 | block.call(@value) 62 | end 63 | 64 | def map(&block) 65 | self.class.new(block.call(@value)) 66 | end 67 | 68 | def full_repr(depth) 69 | "*#{value.repr(depth)}" 70 | end 71 | 72 | def inspect 73 | "*#{value.inspect}" 74 | end 75 | end 76 | 77 | class ListRecAttr < Attr 78 | def initialize(*) 79 | super 80 | raise ArgumentError.new("Expected a list, got #{@value.inspect}") unless @value.is_a? Array 81 | end 82 | 83 | def each(&block) 84 | @value.each(&block) 85 | end 86 | 87 | def map(&block) 88 | ListRecAttr.new(@value.map(&block)) 89 | end 90 | 91 | def full_repr(depth) 92 | "**[#{value.map { |e| e.repr(depth) }.join(' ')}]" 93 | end 94 | 95 | def inspect 96 | "**#{value.inspect}" 97 | end 98 | end 99 | 100 | class OptAttr < RecAttr 101 | def each(&block) 102 | return if @value.nil? 103 | super 104 | end 105 | 106 | def map(&block) 107 | return self if @value.nil? 108 | super 109 | end 110 | 111 | def full_repr(depth) 112 | return '_' if @value.nil? 113 | super 114 | end 115 | 116 | def inspect 117 | "*#{value.inspect}" 118 | end 119 | end 120 | 121 | class Node 122 | class << self 123 | def attrs 124 | @attrs ||= [] 125 | end 126 | 127 | def types 128 | @types ||= [] 129 | end 130 | 131 | def short_name 132 | @short_name ||= begin 133 | self.name =~ /(?:.*::)?(.*)/ 134 | $1.gsub(/([[:lower:]])([[:upper:]])/) { "#{$1}_#{$2.downcase}" } 135 | .downcase 136 | end 137 | end 138 | 139 | def defattr(name, type) 140 | attrs << name 141 | types << type 142 | index = attrs.size - 1 143 | define_method name do 144 | attrs[index].value 145 | end 146 | end 147 | 148 | def defdata(name) 149 | defattr(name, DataAttr) 150 | end 151 | 152 | def defrec(name) 153 | defattr(name, RecAttr) 154 | end 155 | 156 | def deflistrec(name) 157 | defattr(name, ListRecAttr) 158 | end 159 | 160 | def defopt(name) 161 | defattr(name, OptAttr) 162 | end 163 | 164 | def make(*attrs) 165 | if attrs.size != types.size 166 | raise ArgumentError.new("Expected #{types.size} arguments, got #{attrs.size}") 167 | end 168 | 169 | new(attrs.zip(types).map { |(value, type)| 170 | type.new(value) 171 | }) 172 | end 173 | 174 | alias [] make 175 | end 176 | 177 | attr_reader :attrs 178 | def initialize(attrs) 179 | @attrs = attrs 180 | end 181 | 182 | def each(&block) 183 | @attrs.each do |attr| 184 | attr.each(&block) 185 | end 186 | end 187 | 188 | def map(&block) 189 | self.class.new(@attrs.map { |attr| attr.map(&block) }) 190 | end 191 | 192 | # Object equality based on 193 | # https://stackoverflow.com/questions/1931604/whats-the-right-way-to-implement-equality-in-ruby 194 | def ==(o) 195 | o.class == self.class && o.attrs.zip(@attrs).map { |(obj_attr, attr)| obj_attr == attr }.all? 196 | end 197 | 198 | alias eql? == 199 | 200 | def inspect 201 | "#<#{self.class}#{attrs.inspect}>" 202 | end 203 | 204 | def repr(depth=nil) 205 | depth ||= 1.0/0 206 | "##{self.class.short_name}(#{attrs.map { |a| a.repr(depth) }.join(' ')})" 207 | end 208 | 209 | def hash 210 | [self.class, *self.attrs].hash 211 | end 212 | 213 | def accept(visitor, *args, &block) 214 | visitor.send(:"visit_#{self.class.short_name}", self, *args, &block) 215 | end 216 | end 217 | 218 | class Visitor 219 | def visit(node, *args, &block) 220 | node.accept(self, *args, &block) 221 | end 222 | 223 | def visit_default(node, *args, &block) 224 | node.map { |child| visit(child, *args, &block) } 225 | end 226 | 227 | def method_missing(method_name, *args, &block) 228 | if method_name =~ /^visit_/ 229 | visit_default(*args, &block) 230 | else 231 | super 232 | end 233 | end 234 | end 235 | 236 | class Collector < Visitor 237 | attr_reader :collection 238 | def initialize 239 | @collection = Hash.new 240 | end 241 | 242 | def visit(node, *args) 243 | collection[node] ||= node.accept(self, *args) 244 | end 245 | 246 | def visit_default(node, *args) 247 | collect_from(node, *args) 248 | end 249 | 250 | def collect(node, *args) 251 | visit(node, *args) 252 | collection 253 | end 254 | 255 | def collect_one(node, *args) 256 | collect(node, *args)[node] 257 | end 258 | 259 | protected 260 | def collect_from(enum, *args) 261 | out = Set.new 262 | enum.each do |child| 263 | out.merge(visit(child, *args)) 264 | end 265 | out 266 | end 267 | end 268 | 269 | class Walker < Visitor 270 | def visit_default(node, *args, &block) 271 | node.each { |child| visit(child, *args, &block) } 272 | nil 273 | end 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /lib/magc/value.rb: -------------------------------------------------------------------------------- 1 | 2 | module Magritte 3 | module Value 4 | 5 | class Base 6 | def call(args, range) 7 | Proc.current.crash!("Can't call this! (#{self.repr}) (at #{range.repr})") 8 | end 9 | 10 | def bytecode_call(frame, args, range) 11 | frame.proc.crash("Can't call this! (#{self.repr}) (at #{range.repr})") 12 | end 13 | 14 | def typename 15 | raise 'abstract' 16 | end 17 | end 18 | 19 | class String < Base 20 | attr_reader :value 21 | 22 | def to_s 23 | @value 24 | end 25 | 26 | def typename 27 | 'string' 28 | end 29 | 30 | def initialize(value) 31 | @value = value 32 | end 33 | 34 | def ==(other) 35 | other.is_a?(self.class) && other.value == @value 36 | end 37 | 38 | def eql?(other) 39 | self == other 40 | end 41 | 42 | def hash 43 | "#string:#{@value}".hash 44 | end 45 | 46 | def call(args, range) 47 | # Semantics: Look up the thing in current env and call it 48 | # Proc.current.env 49 | Proc.current.env.get(@value).call(args, range) 50 | rescue Env::MissingVariable => e 51 | Proc.current.crash!(e.to_s) 52 | end 53 | 54 | def repr 55 | if @value =~ /[\r\n\\]/ 56 | @value.inspect 57 | else 58 | value 59 | end 60 | end 61 | end 62 | 63 | class Number < Base 64 | attr_reader :value 65 | 66 | def initialize(value) 67 | @value = value.to_f 68 | end 69 | 70 | def typename 71 | 'number' 72 | end 73 | 74 | def repr 75 | value.to_s.gsub(/\.0$/, '') 76 | end 77 | 78 | def eql?(other) 79 | self.class == other.class && @value == other.value 80 | end 81 | 82 | def hash 83 | "#number:#{@value.to_f}".hash 84 | end 85 | 86 | def to_s 87 | repr 88 | end 89 | 90 | def ==(other) 91 | other.is_a?(self.class) && other.value == @value 92 | end 93 | end 94 | 95 | class Vector < Base 96 | attr_reader :elems 97 | 98 | def initialize(elems) 99 | @elems = elems.freeze 100 | end 101 | 102 | def typename 103 | 'vector' 104 | end 105 | 106 | def call(args, range) 107 | head, *rest = (@elems + args) 108 | Proc.current.crash!("Empty call") if head.nil? 109 | head.call(rest, range) 110 | end 111 | 112 | def repr 113 | "[#{elems.map(&:repr).join(" ")}]" 114 | end 115 | 116 | def to_s 117 | repr 118 | end 119 | 120 | def ==(other) 121 | other.is_a?(self.class) && other.elems.size == @elems.size && 122 | other.elems.zip(@elems).map { |le, re| le == re }.all? 123 | end 124 | end 125 | 126 | class Environment < Base 127 | attr_reader :env 128 | 129 | def typename 130 | 'env' 131 | end 132 | 133 | def initialize(env) 134 | @env = env 135 | end 136 | 137 | def repr 138 | env.repr 139 | end 140 | 141 | def to_s 142 | repr 143 | end 144 | end 145 | 146 | class Channel < Base 147 | attr_reader :channel 148 | 149 | def initialize(channel) 150 | @channel = channel 151 | end 152 | 153 | def typename 154 | 'channel' 155 | end 156 | 157 | def repr 158 | "" 159 | end 160 | 161 | def to_s 162 | repr 163 | end 164 | 165 | def ==(other) 166 | other.is_a?(self.class) && other.channel == @channel 167 | end 168 | end 169 | 170 | class Function < Base 171 | attr_reader :name 172 | attr_reader :env 173 | attr_reader :patterns 174 | attr_reader :expr 175 | 176 | def initialize(name, env, patterns, expr) 177 | @name = name 178 | @env = env 179 | @patterns = patterns 180 | @expr = expr 181 | 182 | if @patterns.size != @expr.size 183 | raise "malformed function (#{@patterns.size} patterns, #{@expr.size} bodies" 184 | end 185 | end 186 | 187 | def typename 188 | 'function' 189 | end 190 | 191 | def call(args, range) 192 | env = @env.splice(Proc.current.env) 193 | bound, consequent = match!(args, env) 194 | Proc.current.with_trace(self, range) do 195 | Proc.enter_frame(bound) { Interpret.interpret(consequent) } 196 | end 197 | end 198 | 199 | def match!(args, env) 200 | match(args, env) or Proc.current.crash!("Pattern match failed on #{self.repr} for #{args.repr}") 201 | end 202 | 203 | def match(args, env) 204 | args = Vector.new(args) 205 | @patterns.each_with_index do |pat, i| 206 | bound = Pattern.evaluate(pat, args, env) 207 | return [bound, @expr[i]] if bound 208 | end 209 | 210 | false 211 | end 212 | 213 | def repr 214 | "" 215 | end 216 | 217 | def to_s 218 | repr 219 | end 220 | end 221 | 222 | class BytecodeFunction < Base 223 | def bytecode_call(frame, args, range) 224 | new_frame = create_env(args, frame.env.extend) 225 | frame.proc.call_stack 226 | BytecodeProc.current.enter_frame(self, args) 227 | end 228 | end 229 | 230 | class BuiltinFunction < Base 231 | attr_reader :block 232 | attr_reader :name 233 | 234 | def initialize(name, block) 235 | @name = name 236 | @block = block 237 | end 238 | 239 | def typename 240 | 'builtin' 241 | end 242 | 243 | def call(args, range) 244 | Proc.current.with_trace(self, range) do 245 | Std.instance_exec(*args, &@block) || Status.normal 246 | end 247 | rescue Builtins::ArgError => e 248 | Proc.current.crash!(e.to_s) 249 | end 250 | 251 | def repr 252 | "" 253 | end 254 | 255 | def to_s 256 | repr 257 | end 258 | end 259 | 260 | class Compensation < Base 261 | attr_reader :action 262 | attr_reader :range 263 | attr_reader :uncond 264 | 265 | def initialize(action, range, uncond) 266 | @action = action 267 | @range = range 268 | @uncond = uncond 269 | end 270 | 271 | def typename 272 | 'compensation' 273 | end 274 | 275 | def run 276 | @action.call([], @range) 277 | end 278 | 279 | def run_checkpoint 280 | run if @uncond == :unconditional 281 | end 282 | end 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /lib/magvm/actions.py: -------------------------------------------------------------------------------- 1 | from rpython.rlib.objectmodel import enforceargs 2 | import rpython.rtyper.lltypesystem.lltype as lltype 3 | from inst import inst_type_table, InstType 4 | from util import as_dashed 5 | from value import * 6 | from debug import debug, open_shell 7 | from const import const_table 8 | from intrinsic import intrinsics 9 | from env import Env 10 | from symbol import revsym # error messages only 11 | from status import Success, Fail 12 | 13 | inst_actions = [None] * len(inst_type_table) 14 | inst_action_sig = enforceargs(None, lltype.Array(lltype.Signed)) 15 | 16 | def inst_action(fn): 17 | """NOT_RPYTHON""" 18 | inst_type = as_dashed(fn.__name__) 19 | debug(0, ['-- inst_type', inst_type]) 20 | 21 | inst_id = inst_type_table.get(inst_type).id 22 | # inst_actions[inst_id] = inst_action_sig(fn) 23 | inst_actions[inst_id] = fn 24 | 25 | return fn 26 | 27 | 28 | ################## actions! ################# 29 | # These functions implement the instructions 30 | # of the vm, and are run by a Frame whenever 31 | # it encounters the corresponding instruction. 32 | # 33 | # see inst.py for descriptions of these. 34 | # see frame.py, specifically the .step() method 35 | # to see how they're called. 36 | 37 | @inst_action 38 | def pop(frame, args): 39 | frame.stack.pop() 40 | 41 | @inst_action 42 | def noop(frame, args): 43 | pass 44 | 45 | @inst_action 46 | def swap(frame, args): 47 | x = frame.pop() 48 | y = frame.pop() 49 | frame.push(x) 50 | frame.push(y) 51 | 52 | @inst_action 53 | def dup(frame, args): 54 | debug(0, ['-- dup', frame.s()]) 55 | frame.push(frame.top()) 56 | 57 | @inst_action 58 | def frame(frame, args): 59 | env = frame.pop_env() 60 | addr = args[0] 61 | debug(0, ['-- frame', env.s()]) 62 | frame.proc.frame(env, addr) 63 | 64 | @inst_action 65 | def spawn(frame, args): 66 | addr = args[0] 67 | env = frame.pop_env() 68 | new_proc = frame.proc.machine.spawn(env, addr) 69 | debug(0, ['-- spawn', env.s(), new_proc.s()]) 70 | 71 | @inst_action 72 | def collection(frame, args): 73 | frame.push(Vector([])) 74 | 75 | @inst_action 76 | def const(frame, args): 77 | frame.push(const_table.lookup(args[0])) 78 | 79 | @inst_action 80 | def collect(frame, args): 81 | value = frame.pop() 82 | collection = frame.top_vec() 83 | collection.push(value) 84 | 85 | @inst_action 86 | def splat(frame, args): 87 | vec = frame.pop_vec() 88 | collection = frame.top_vec() 89 | 90 | for v in vec.values: 91 | if isinstance(v, Vector): 92 | collection.push_all(v.values) 93 | else: 94 | collection.push(v) 95 | 96 | @inst_action 97 | def index(frame, args): 98 | idx = args[0] 99 | source = frame.pop_vec() 100 | frame.push(source.values[idx]) 101 | 102 | @inst_action 103 | def current_env(frame, args): 104 | frame.push(frame.env) 105 | 106 | @inst_action 107 | def let(frame, args): 108 | val = frame.pop() 109 | env = frame.pop() 110 | sym = args[0] 111 | debug(0, [revsym(sym), '=', val.s()]) 112 | env.let(sym, val) 113 | 114 | @inst_action 115 | def env(frame, args): 116 | frame.push(Env()) 117 | 118 | @inst_action 119 | def ref(frame, args): 120 | env = frame.pop_env() 121 | try: 122 | frame.push(env.lookup_ref(args[0])) 123 | except KeyError: 124 | frame.fail(tagged('missing-key', env, String(revsym(args[0])))) 125 | 126 | @inst_action 127 | def dynamic_ref(frame, args): 128 | debug(0, [frame.s()]) 129 | lookup = frame.pop_string() 130 | env = frame.pop_env() 131 | debug(0, ['-- dynamic-ref lookup:', lookup, env.s()]) 132 | 133 | try: 134 | frame.push(env.lookup_ref(sym(lookup))) 135 | except KeyError: 136 | ref = env.let(sym(lookup), placeholder) 137 | frame.push(ref) 138 | 139 | @inst_action 140 | def ref_get(frame, args): 141 | ref = frame.pop_ref() 142 | val = ref.ref_get() 143 | 144 | if val == placeholder: 145 | frame.fail(tagged('uninitialized-ref', ref)) 146 | 147 | frame.push(val) 148 | 149 | @inst_action 150 | def ref_set(frame, args): 151 | val = frame.pop() 152 | ref = frame.pop_ref() 153 | ref.ref_set(val) 154 | 155 | @inst_action 156 | def jump(frame, args): 157 | frame.pc = args[0] 158 | 159 | @inst_action 160 | def jumpne(frame, args): 161 | lhs = frame.pop() 162 | rhs = frame.pop() 163 | 164 | debug(0, ['-- jumpne', lhs.s(), rhs.s()]) 165 | 166 | # TODO: define equality properly! 167 | if lhs.s() == rhs.s(): return 168 | 169 | frame.pc = args[0] 170 | 171 | @inst_action 172 | def jumplt(frame, args): 173 | limit = frame.pop_number() 174 | val = frame.pop_number() 175 | 176 | debug(0, ['-- jumplt', str(val), '<', str(limit)]) 177 | 178 | if val < limit: 179 | frame.pc = args[0] 180 | 181 | @inst_action 182 | def return_(frame, args): 183 | proc = frame.proc 184 | proc.pop() 185 | debug(0, ['-- returned', proc.s()]) 186 | 187 | for (addr, is_unconditional) in frame.compensations: 188 | debug(0, ['-- ret-comp', ('(run!)' if is_unconditional else '(skip)'), labels_by_addr[addr].name]) 189 | if is_unconditional: proc.frame(frame.env, addr) 190 | 191 | if not proc.frames: 192 | proc.set_done() 193 | 194 | @inst_action 195 | def invoke(frame, args): 196 | frame.proc.status = Success() 197 | collection = frame.pop_vec() 198 | if not collection.values: 199 | frame.fail_str('empty-invocation') 200 | 201 | debug(0, ['-- invoke', collection.s()]) 202 | 203 | # tail elim is handled in proc.py 204 | # if frame.current_inst().id == InstType.RETURN: 205 | # frame.proc.frames.pop() 206 | 207 | invokee = collection.values.pop(0) 208 | if not invokee.invokable: frame.fail(tagged('not-invokable', invokee)) 209 | 210 | invokee.invokable.invoke(frame, collection) 211 | 212 | @inst_action 213 | def closure(frame, args): 214 | addr = args[0] 215 | env = frame.pop_env() 216 | frame.push(Function(env, addr)) 217 | 218 | @inst_action 219 | def env_collect(frame, args): 220 | env = frame.pop_env() 221 | collection = frame.pop_vec() 222 | env.set_output(0, collection) 223 | frame.push(collection) 224 | frame.push(env) 225 | 226 | @inst_action 227 | def wait_for_close(frame, args): 228 | collection = frame.top_vec() 229 | 230 | if collection.writer_count == 0: return 231 | 232 | frame.proc.machine.channels.register(collection) 233 | collection.wait_for_close(frame.proc) 234 | debug(0, ['-- wait-for-close', collection.s(), frame.proc.s()]) 235 | 236 | @inst_action 237 | def env_extend(frame, args): 238 | env = frame.pop_env() 239 | frame.push(env.extend()) 240 | 241 | @inst_action 242 | def env_unhinge(frame, args): 243 | env = frame.pop_env() 244 | frame.push(env.unhinge()) 245 | 246 | @inst_action 247 | def channel(frame, args): 248 | frame.push(frame.proc.machine.make_channel()) 249 | 250 | @inst_action 251 | def env_pipe(frame, args): 252 | channel = frame.pop_channel() 253 | env = frame.pop_env() 254 | producer = env.extend() 255 | producer.set_output(0, channel) 256 | consumer = env.extend() 257 | consumer.set_input(0, channel) 258 | debug(0, ['-- pipe %s | %s' % (producer.s(), consumer.s())]) 259 | frame.push(consumer) 260 | frame.push(producer) 261 | 262 | @inst_action 263 | def env_set_input(frame, args): 264 | idx = args[0] 265 | inp = frame.pop_channel() 266 | env = frame.pop_env() 267 | env.set_input(idx, inp) 268 | 269 | @inst_action 270 | def env_set_output(frame, args): 271 | idx = args[0] 272 | outp = frame.pop_channel() 273 | env = frame.pop_env() 274 | env.set_output(idx, outp) 275 | 276 | @inst_action 277 | def intrinsic(frame, args): 278 | try: 279 | builtin = intrinsics.lookup(args[0]) 280 | frame.push(builtin) 281 | except IndexError: 282 | frame.fail(tagged('unknown-intrinsic', String(revsym(args[0])))) 283 | 284 | @inst_action 285 | def rest(frame, args): 286 | size = args[0] 287 | source = frame.pop_vec() 288 | assert size < len(source.values) 289 | assert size >= 0 290 | frame.push(Vector(source.values[size:])) 291 | 292 | @inst_action 293 | def size(frame, args): 294 | source = frame.pop_vec() 295 | frame.push(Int(len(source.values))) 296 | 297 | @inst_action 298 | def typeof(frame, args): 299 | val = frame.pop() 300 | frame.push(String(val.typeof())) 301 | 302 | @inst_action 303 | def crash(frame, args): 304 | reason = frame.pop() 305 | debug(0, ['-- crash: ', frame.proc.s(), reason.s()]) 306 | raise Crash(reason) 307 | 308 | @inst_action 309 | def clear(frame, args): 310 | if len(frame.stack) > 1: 311 | frame.stack.pop(len(frame.stack) - 1) 312 | debug(0, ['-- clear', frame.s()]) 313 | 314 | @inst_action 315 | def last_status(frame, args): 316 | frame.push(frame.proc.status) 317 | 318 | @inst_action 319 | def jumpfail(frame, args): 320 | status = frame.pop_status() 321 | debug(0, ['-- jumpfail', status.s()]) 322 | if not status.is_success(): 323 | frame.pc = args[0] 324 | 325 | @inst_action 326 | def compensate(frame, args): 327 | is_unconditional = (args[1] == 1) 328 | frame.add_compensation(args[0], is_unconditional) 329 | -------------------------------------------------------------------------------- /lib/magvm/base.py: -------------------------------------------------------------------------------- 1 | from env import Env 2 | from util import as_dashed 3 | from symbol import sym 4 | from channel import Streamer 5 | from value import * 6 | from debug import debug 7 | import os 8 | 9 | base_env = Env() 10 | 11 | def global_out(proc, vals): 12 | for val in vals: 13 | debug(0, ['==== GLOBAL_OUT ====', val.s()]) 14 | print val.s() 15 | 16 | base_env.set_output(0, Streamer(global_out)) 17 | -------------------------------------------------------------------------------- /lib/magvm/channel.py: -------------------------------------------------------------------------------- 1 | from value import * 2 | from debug import debug 3 | from status import Status 4 | 5 | class Close(Status): 6 | def __init__(self, channel, is_input): 7 | self.channel = channel 8 | self.is_input = is_input 9 | 10 | def s(self): 11 | direction = 'read' if self.is_input else 'write' 12 | return '' % (self.channel.s(), direction) 13 | 14 | class Blocker(object): 15 | proc = None 16 | 17 | def is_done(self): return True 18 | def s(self): raise NotImplementedError 19 | 20 | class Receiver(Blocker): 21 | def __init__(self, proc, count, into): 22 | self.proc = proc 23 | self.into = into 24 | self.count = count 25 | 26 | def receive(self, val): 27 | self.count -= 1 28 | debug(0, ['remaining', str(self.count), str(self.is_done())]) 29 | if self.is_done(): self.proc.set_running() 30 | 31 | debug(0, ['receiving', val.s(), 'into', self.into.s()]) 32 | self.into.channelable.write(self.proc, val) 33 | 34 | def is_done(self): 35 | return self.count <= 0 36 | 37 | def s(self): 38 | return '' % (self.count, self.proc.s(), self.into.s()) 39 | 40 | 41 | class Sender(Blocker): 42 | def __init__(self, proc, values): 43 | self.proc = proc 44 | self.values = values 45 | self.index = 0 46 | 47 | def next_val(self): 48 | # [jneen] nice thought, but this array might be still in use! 49 | # if we take this approach we have to be sure we copy stuff. should 50 | # see if that makes a perf difference. 51 | # (out, self.values[self.index]) = (self.values[self.index], None) 52 | out = self.values[self.index] 53 | 54 | self.index += 1 55 | return out 56 | 57 | def current_val(self): 58 | return self.values[self.index] 59 | 60 | def is_done(self): 61 | return self.index >= len(self.values) 62 | 63 | def send(self, receiver): 64 | debug(0, ['sending', self.s()]) 65 | 66 | receiver.receive(self.next_val()) 67 | debug(0, ['sender is_done()', str(self.is_done())]) 68 | if self.is_done(): self.proc.try_set_running() 69 | 70 | def s(self): 71 | out = ['') 77 | return ''.join(out) 78 | 79 | 80 | class Channel(Value): 81 | INIT = 0 82 | OPEN = 1 83 | CLOSED = 2 84 | 85 | def __init__(self): 86 | self.writers = [] 87 | self.readers = [] 88 | self.reader_count = 0 89 | self.writer_count = 0 90 | self.senders = [] 91 | self.receivers = [] 92 | self.state = Channel.INIT 93 | self.channelable = Channel.Impl(self) 94 | 95 | def s(self): 96 | return '' % (self.id, self.reader_count, self.writer_count, self.state_name()) 97 | 98 | def typeof(self): return 'channel' 99 | 100 | def state_name(self): 101 | if self.state == Channel.INIT: return 'init' 102 | if self.state == Channel.OPEN: return 'open' 103 | if self.state == Channel.CLOSED: return 'closed' 104 | assert False, 'no such state' 105 | 106 | def is_closed(self): return self.state == Channel.CLOSED 107 | def is_open(self): return self.state != Channel.CLOSED 108 | 109 | def check_for_close(self): 110 | # set up the initial state if we've got readers or writers 111 | if self.state == Channel.INIT and self.reader_count > 0 and self.writer_count > 0: 112 | self.state = Channel.OPEN 113 | return False 114 | 115 | if self.state != Channel.OPEN: return False 116 | 117 | debug(0, ['check_for_close', self.s()]) 118 | if self.reader_count > 0 and self.writer_count > 0: return False 119 | 120 | debug(0, ['closing', self.s()]) 121 | self.state = Channel.CLOSED 122 | 123 | for blocker in self.senders: 124 | blocker.proc.interrupt(Close(self, False)) 125 | 126 | for blocker in self.receivers: 127 | blocker.proc.interrupt(Close(self, True)) 128 | 129 | return True 130 | 131 | @impl 132 | class Impl(Channelable): 133 | def read(self, proc, count, into): 134 | if self.is_closed(): return proc.interrupt(Close(self, True)) 135 | self.receivers.append(Receiver(proc, count, into)) 136 | debug(0, ['-- read set-waiting', str(count)]) 137 | proc.set_waiting() 138 | 139 | def write_all(self, proc, vals): 140 | if self.is_closed(): return proc.interrupt(Close(self, False)) 141 | self.senders.append(Sender(proc, vals)) 142 | debug(0, ['-- write set-waiting', Vector(vals).s()]) 143 | proc.set_waiting() 144 | 145 | def add_writer(self, frame): 146 | debug(0, ['-- add_writer', str(self.writer_count + 1), self.s(), frame.s()]) 147 | self.writer_count += 1 148 | 149 | def add_reader(self, frame): 150 | debug(0, ['-- add_reader', str(self.reader_count + 1), self.s(), frame.s()]) 151 | self.reader_count += 1 152 | 153 | def rm_writer(self, frame): 154 | if self.is_closed(): return 155 | debug(0, ['-- rm_writer', 156 | str(self.writer_count - 1), 157 | self.s(), frame.s()] 158 | + [s.s() for s in self.senders] 159 | + [r.s() for r in self.receivers]) 160 | self.writer_count -= 1 161 | 162 | def rm_reader(self, frame): 163 | if self.is_closed(): return 164 | debug(0, ['-- rm_reader', 165 | str(self.reader_count - 1), 166 | self.s(), frame.s()] 167 | + [s.s() for s in self.senders] 168 | + [r.s() for r in self.receivers]) 169 | self.reader_count -= 1 170 | 171 | def resolve(self): 172 | if self.is_closed(): return False 173 | 174 | while self.senders and self.receivers: 175 | (sender, receiver) = (self.senders.pop(0), self.receivers.pop(0)) 176 | sender.send(receiver) 177 | 178 | if not sender.is_done(): self.senders.insert(0, sender) 179 | if not receiver.is_done(): self.receivers.insert(0, receiver) 180 | 181 | debug(0, ['-- still waiting:['] + [p.s() for p in self.senders + self.receivers] + [']']) 182 | 183 | return self.check_for_close() 184 | 185 | 186 | 187 | class Streamer(Value): 188 | @impl 189 | class Channel(Channelable): 190 | def write_all(self, proc, vals): 191 | self.fn(proc, vals) 192 | 193 | def __init__(self, fn): 194 | self.fn = fn 195 | self.channelable = Streamer.Channel(self) 196 | 197 | def s(self): 198 | return '' 199 | 200 | def typeof(self): return 'streamer' 201 | 202 | -------------------------------------------------------------------------------- /lib/magvm/const.py: -------------------------------------------------------------------------------- 1 | from table import Table 2 | 3 | const_table = Table() 4 | -------------------------------------------------------------------------------- /lib/magvm/debug.py: -------------------------------------------------------------------------------- 1 | from rpython.rlib.objectmodel import we_are_translated 2 | import os 3 | 4 | class Debugger(object): 5 | def __init__(self): 6 | self.fd = -1 7 | self.level = -1 8 | self.fname = None 9 | 10 | def should_debug(self, level): 11 | return level <= self.level 12 | 13 | def setup(self): 14 | if self.fname: self.open_file() 15 | 16 | def open_file(self): 17 | self.fd = os.open(self.fname, os.O_TRUNC | os.O_WRONLY | os.O_CREAT, 0o777) 18 | 19 | def debug(self, level, parts): 20 | assert isinstance(level, int) 21 | 22 | if self.should_debug(level) and self.fd > 0: 23 | os.write(self.fd, ' '.join(parts) + '\n') 24 | 25 | debugger = Debugger() 26 | debug_enabled = True 27 | 28 | try: 29 | debug_option = os.environ['MAGRITTE_DEBUG'] 30 | should_debug = True 31 | 32 | if debug_option in ['1', 'stdout']: 33 | debugger.fd = 1 # stdout 34 | elif debug_option in ['2', 'stderr']: 35 | debugger.fd = 2 36 | elif debug_option in ['off', 'none', '0']: 37 | debug_enabled = False 38 | else: 39 | debugger.fname = debug_option 40 | 41 | except KeyError: 42 | debug_enabled = False 43 | 44 | def debug(level, parts): 45 | if not debug_enabled: return 46 | debugger.debug(level, parts) 47 | 48 | def set_debug(level): 49 | if not debug_enabled: return 50 | debugger.level = level 51 | 52 | def disable_debug(): 53 | if not debug_enabled: return 54 | debugger.level = -1 55 | 56 | def open_debug_file(fname): 57 | debugger.fname = fname 58 | debugger.open_file() 59 | 60 | def open_shell(frame, args): 61 | if we_are_translated(): 62 | vm_debugger_rpython(frame, args) 63 | else: 64 | vm_debugger_dynamic(frame, args) 65 | 66 | def vm_debugger_rpython(frame, args): 67 | print 'vm_debugger not available in compiled mode' 68 | 69 | def vm_debugger_dynamic(frame, args): 70 | """NOT_RPYTHON""" 71 | import code 72 | 73 | code.interact(local=dict(globals(), **locals())) 74 | -------------------------------------------------------------------------------- /lib/magvm/env.py: -------------------------------------------------------------------------------- 1 | from value import * 2 | from symbol import revsym 3 | 4 | ############ environments ############### 5 | # An environment in Magritte is a simple 6 | # dictionary of keys and values, together 7 | # with a parent pointer (a "prototype"). 8 | # These are used not only for variable 9 | # scopes, but also for all OOP-style 10 | # patterns. 11 | # 12 | # An environment additionally contains 13 | # input and output channels, which can 14 | # be inherited. 15 | # 16 | # see frame.py for how the channels are used 17 | # see actions.py and intrinsics.py for 18 | # the basic operations from the machine. 19 | MAX_CHANNELS = 8 20 | class Env(Value): 21 | def __init__(self, parent=None): 22 | assert parent is None or isinstance(parent, Env) 23 | self.parent = parent 24 | self.inputs = [None] * MAX_CHANNELS 25 | self.outputs = [None] * MAX_CHANNELS 26 | self.dict = {} 27 | 28 | def as_dict(self): 29 | out = {} 30 | for (k, v) in self.dict.iteritems(): 31 | out[revsym(k)] = v.ref_get() 32 | 33 | return out 34 | 35 | def s(self): 36 | out = ['{ '] 37 | is_first = True 38 | for (k, v) in self.as_dict().iteritems(): 39 | if not is_first: out.append('; ') 40 | is_first = False 41 | 42 | out.append('%s = %s' % (k, v.s())) 43 | 44 | inp = self.get_input(0) 45 | if inp: out.append('; in:%s' % inp.s()) 46 | outp = self.get_output(0) 47 | if outp: out.append('; out:%s' % outp.s()) 48 | 49 | out.append(' }') 50 | 51 | return ''.join(out) 52 | 53 | def __str__(self): 54 | return self.s() 55 | 56 | def extend(self): 57 | return Env(self) 58 | 59 | def unhinge(self): 60 | return Env(None).merge(self) 61 | 62 | def merge(self, other): 63 | assert isinstance(other, Env) 64 | 65 | # copy the *refs* here 66 | for (k, v) in other.dict.iteritems(): 67 | self.dict[k] = v 68 | 69 | # TODO: all channels 70 | if other.get_input(0): self.set_input(0, other.get_input(0)) 71 | if other.get_output(0): self.set_output(0, other.get_output(0)) 72 | 73 | return self 74 | 75 | def get_input(self, i): 76 | return self.inputs[i] or (self.parent and self.parent.get_input(i)) 77 | 78 | def has_input(self, ch): 79 | if ch in self.inputs: return True 80 | if not self.parent: return False 81 | return self.parent.has_input(ch) 82 | 83 | def has_output(self, ch): 84 | if ch in self.outputs: return True 85 | if not self.parent: return False 86 | return self.parent.has_output(ch) 87 | 88 | def has_channel(self, is_input, channel): 89 | if is_input: return self.has_input(channel) 90 | else: return self.has_output(channel) 91 | 92 | def get_output(self, i): 93 | return self.outputs[i] or (self.parent and self.parent.get_output(i)) 94 | 95 | def set_input(self, i, ch): 96 | assert ch.channelable 97 | debug(0, ['set_input', self.s(), ch.s()]) 98 | self.inputs[i] = ch 99 | 100 | def set_output(self, i, ch): 101 | assert ch.channelable 102 | debug(0, ['set_output', self.s(), ch.s()]) 103 | self.outputs[i] = ch 104 | 105 | def each_input(self, fn, *a): 106 | for i in range(0, MAX_CHANNELS): 107 | input = self.get_input(i) 108 | if not input: return 109 | fn(input, *a) 110 | 111 | def each_output(self, fn, *a): 112 | for i in range(0, MAX_CHANNELS): 113 | output = self.get_output(i) 114 | if not output: return 115 | fn(output, *a) 116 | 117 | def lookup_ref(self, key): 118 | try: 119 | return self.dict[key] 120 | except KeyError: 121 | if self.parent: 122 | return self.parent.lookup_ref(key) 123 | else: 124 | raise KeyError(revsym(key)) 125 | 126 | def has(self, key): 127 | try: 128 | self.lookup_ref(key) 129 | return True 130 | except KeyError: 131 | return False 132 | 133 | def let(self, key, val): 134 | r = Ref(val) 135 | self.dict[key] = r 136 | return r 137 | 138 | def get(self, key): 139 | return self.lookup_ref(key).ref_get() 140 | 141 | def mut(self, key, val): 142 | return self.lookup_ref(key).ref_set(val) 143 | 144 | def typeof(self): return 'env' 145 | -------------------------------------------------------------------------------- /lib/magvm/frame.py: -------------------------------------------------------------------------------- 1 | from env import Env 2 | from status import Status, Success, Fail 3 | from labels import labels_by_addr, inst_table 4 | from inst import inst_type_table, InstType 5 | from debug import debug 6 | from load import arg_as_str 7 | from actions import inst_actions 8 | from value import * 9 | 10 | ################# frame ##################### 11 | # This class represents a stack frame inside 12 | # a single process. It contains a program 13 | # counter which points to the current 14 | # instruction, and an environment holding the 15 | # variables in its local scope. 16 | # 17 | # The frame also represents the main API for 18 | # intrinsics and instructions (actions) to 19 | # change the state of the machine - the 20 | # current frame is always passed in to their 21 | # implementations. 22 | # 23 | # see env.py for environments 24 | # see proc.py for the process 25 | # see actions.py and intrinsic.py for 26 | # the use of many of these methods 27 | 28 | class Frame(object): 29 | def crash(self, message): 30 | assert isinstance(message, Status) 31 | raise Crash(message) 32 | 33 | def fail(self, reason): 34 | raise Crash(Fail(reason)) 35 | 36 | def fail_str(self, reason_str): 37 | raise Crash(Fail(String(reason_str))) 38 | 39 | def add_compensation(self, addr, is_unconditional): 40 | debug(0, ['-- add-compensation', self.s(), labels_by_addr[addr].name, str(is_unconditional)]) 41 | self.compensations.append((addr, is_unconditional)) 42 | 43 | def __init__(self, proc, env, addr): 44 | assert isinstance(env, Env) 45 | assert isinstance(addr, int) 46 | self.proc = proc 47 | self.env = env 48 | self.pc = self.addr = addr 49 | self.stack = [] 50 | self.compensations = [] 51 | 52 | def s(self): 53 | out = ['') 68 | return ''.join(out) 69 | 70 | def __str__(self): 71 | return self.s() 72 | 73 | def setup(self): 74 | self.env.each_input(register_as_input, self) 75 | self.env.each_output(register_as_output, self) 76 | 77 | def set_status(self, status): 78 | debug(0, ['-- set status', status.s()]) 79 | self.proc.status = status 80 | 81 | def cleanup(self): 82 | debug(0, ['-- cleanup', self.s()]) 83 | self.env.each_input(deregister_as_input, self) 84 | self.env.each_output(deregister_as_output, self) 85 | is_success = self.proc.status.is_success() 86 | self.proc.last_cleaned.append(self) 87 | 88 | def push(self, val): 89 | assert val is not None, 'pushing None onto the stack' 90 | 91 | self.stack.append(val) 92 | 93 | def pop(self): 94 | if not self.stack: raise Crash(Fail(String('empty-stack'))) 95 | val = self.stack.pop() 96 | assert isinstance(val, Value) 97 | return val 98 | 99 | def pop_number(self): 100 | return self.pop().as_number(self) 101 | 102 | def pop_string(self, message='not-a-string'): 103 | val = self.pop() 104 | if not isinstance(val, String): self.fail(tagged(message, val)) 105 | return val.value 106 | 107 | def pop_status(self, message='not-a-status'): 108 | val = self.pop() 109 | if not isinstance(val, Status): self.fail(tagged(message, val)) 110 | return val 111 | 112 | def pop_env(self, message='not-an-env'): 113 | val = self.pop() 114 | if not isinstance(val, Env): self.fail(tagged(message, val)) 115 | return val 116 | 117 | def pop_ref(self, message='not-a-ref'): 118 | val = self.pop() 119 | if not isinstance(val, Ref): self.fail(tagged(message, val)) 120 | return val 121 | 122 | def pop_channel(self, message='not-a-channel'): 123 | val = self.pop() 124 | if not val.channelable: self.fail(tagged(message, val)) 125 | return val 126 | 127 | def pop_vec(self, message='not-a-vector'): 128 | val = self.pop() 129 | if not isinstance(val, Vector): self.fail(tagged(message, val)) 130 | return val 131 | 132 | def top_vec(self, message='not-a-vector'): 133 | val = self.top() 134 | if not isinstance(val, Vector): self.fail(tagged(message, val)) 135 | return val 136 | 137 | def top(self): 138 | return self.stack[len(self.stack)-1] 139 | 140 | def put(self, vals): 141 | debug(0, ['-- put', self.env.get_output(0).s()] + [v.s() for v in vals]) 142 | self.env.get_output(0).channelable.write_all(self.proc, vals) 143 | 144 | def get(self, into, n=1): 145 | self.env.get_input(0).channelable.read(self.proc, n, into) 146 | 147 | def label_name(self): 148 | return labels_by_addr[self.addr].name 149 | 150 | # @enforceargs(None, lltype.Signed, lltype.Array(lltype.Signed)) 151 | def run_inst_action(self, inst_id, static_args): 152 | inst_type = inst_type_table.lookup(inst_id) 153 | debug(0, ['+', inst_type.name] + 154 | [arg_as_str(inst_type, i, arg) for (i, arg) in enumerate(static_args)]) 155 | 156 | action = inst_actions[inst_id] 157 | if not action: 158 | raise NotImplementedError('action for: '+inst_type_table.lookup(inst_id).name) 159 | 160 | for arg in static_args: 161 | assert isinstance(arg, int) 162 | 163 | action(self, static_args) 164 | 165 | def step(self): 166 | inst = self.current_inst() 167 | self.pc += 1 168 | self.run_inst_action(inst.inst_id, inst.arguments) 169 | 170 | def should_eliminate(self): 171 | return self.current_inst().inst_id == InstType.RETURN 172 | 173 | def current_inst(self): 174 | return inst_table.lookup(self.pc) 175 | 176 | def register_as_input(ch, frame): ch.channelable.add_reader(frame) 177 | def register_as_output(ch, frame): ch.channelable.add_writer(frame) 178 | def deregister_as_input(ch, frame): ch.channelable.rm_reader(frame) 179 | def deregister_as_output(ch, frame): ch.channelable.rm_writer(frame) 180 | 181 | -------------------------------------------------------------------------------- /lib/magvm/inst.py: -------------------------------------------------------------------------------- 1 | from table import Table, TableEntry 2 | from symbol import revsym 3 | from intrinsic import intrinsics 4 | 5 | class InstType(TableEntry): 6 | def __init__(self, name, static_types, in_types, out_types, doc): 7 | self.name = name 8 | self.static_types = static_types 9 | self.in_types = in_types 10 | self.out_types = out_types 11 | 12 | # important step for loading more than one file: re-index all references 13 | # to constants and addresses relative to the global table. 14 | def reindex(self, arr, offsets, symbol_translation): 15 | for (i, tname) in enumerate(self.static_types): 16 | if tname: 17 | # special case for symbols - we have to re-use 18 | # existing symbol ids, so the translation has to 19 | # be done manually 20 | if tname == 'sym': 21 | arr[i] = symbol_translation[arr[i]] 22 | continue 23 | 24 | # special case for intrinsics, which are a static table 25 | if tname == 'intrinsic': 26 | intrinsic_name = revsym(symbol_translation[arr[i]]) 27 | new_id = intrinsics.get(intrinsic_name).id 28 | arr[i] = intrinsics.get(intrinsic_name).id 29 | continue 30 | 31 | arr[i] += offsets[tname] 32 | 33 | return arr 34 | 35 | class Inst(TableEntry): 36 | name = None 37 | 38 | def __init__(self, inst_id, arguments): 39 | for a in arguments: 40 | assert isinstance(a, int) 41 | assert a >= 0 42 | 43 | self.inst_id = inst_id 44 | assert isinstance(arguments, list) 45 | self.arguments = arguments 46 | 47 | def type(self): 48 | return inst_type_table.lookup(self.inst_id) 49 | 50 | inst_type_table = Table() 51 | def mkinst(name, *a): 52 | t = InstType(name, *a) 53 | inst_type_table.register(t) 54 | 55 | screaming_name = name.upper().replace('-', '_') 56 | setattr(InstType, screaming_name, t.id) 57 | 58 | ############# instruction documentation ############### 59 | # mkinst(instruction_name, argument_types, start_stack, end_stack, description) 60 | # 61 | # The argument types are needed at runtime for loading 62 | # files, so we can re-index them according to the global 63 | # tables. For example, in a compiled .magc file a `jump` 64 | # instruction may refer to an address within the file - 65 | # but once all instructions are merged together into a 66 | # global table it will need to refer to a different number. 67 | # 68 | # see actions.py for the implementations of these. 69 | 70 | # jumps and spawns 71 | mkinst('frame', ['inst'], ['env'], [], 'start a new frame') 72 | mkinst('return', [], [], [], 'pop a frame off the stack') 73 | mkinst('spawn', ['inst'], ['env'], [], 'spawn a new process') 74 | mkinst('jump', ['inst'], [], [], 'jump') 75 | mkinst('jumpne', ['inst'], [None, None], [], 'jump if not equal') 76 | mkinst('jumplt', ['inst'], ['int', 'int'], [], 'jump if less than') 77 | mkinst('invoke', [], ['collection'], [], 'invoke a collection') 78 | mkinst('jumpfail', ['inst'], ['status'], [], 'jump if the last status is a failure') 79 | 80 | # vectors and collections 81 | mkinst('collection', [], [], ['collection'], 'start a collection') 82 | mkinst('index', [None], ['vec', 'idx'], [], 'index a vector') 83 | mkinst('collect', [], ['collection', None], ['collection'], 'collect a single value into a collection') 84 | mkinst('splat', [], ['collection', 'collection'], ['collection'], 'append all elements of the vector to the collection') 85 | mkinst('typeof', [], [None], ['str'], 'get the type of a value') 86 | 87 | # environments and refs 88 | mkinst('env', [], [], ['env'], 'make a new env') 89 | mkinst('current-env', [], [], ['env'], 'load the current environment') 90 | mkinst('ref', ['sym'], ['env'], ['ref'], 'load a ref from a collection') 91 | mkinst('ref-get', [], ['ref'], [None], 'load a value from a ref') 92 | mkinst('ref-set', [], ['ref', None], [], 'write a value to a ref') 93 | mkinst('dynamic-ref', [], ['env', 'string'], [None], 'dynamically look up a ref') 94 | mkinst('env-extend', [], ['env'], ['env'], 'extend an environment') 95 | mkinst('env-collect', [], ['collection', 'env'], ['collection', 'env'], 'set up an environment to write to a collection') 96 | mkinst('env-pipe', [None, None], ['env', 'channel'], ['env', 'env'], 'make a producer and a consumer env from a channel') 97 | mkinst('env-merge', [], ['env', 'env'], ['env'], 'merge an env into another (mutates first)') 98 | mkinst('env-unhinge', [], ['env'], ['env'], 'unhinge an env from its parent') 99 | mkinst('let', ['sym'], ['env', None], [], 'make a new binding in an env') 100 | 101 | # tables 102 | mkinst('const', ['const'], [], [None], 'load a constant') 103 | 104 | # stack manip 105 | mkinst('swap', [], [None, None], [None, None], 'swap') 106 | mkinst('dup', [], [None], [None, None], 'dup') 107 | mkinst('pop', [], [None], [], 'pop') 108 | 109 | # functions 110 | mkinst('closure', ['inst'], ['env'], ['closure'], 'make a new closure') 111 | mkinst('last-status', [], [], ['status'], 'load the last status') 112 | mkinst('intrinsic', ['intrinsic'], [], ['intrinsic'], 'load an intrinsic fn') 113 | 114 | # channels 115 | mkinst('channel', [], [], ['channel'], 'make a new channel') 116 | mkinst('env-set-output', [None], ['env', 'channel'], [], 'set the output on an env') 117 | mkinst('env-set-input', [None], ['env', 'channel'], [], 'set the input on an env') 118 | mkinst('crash', [], ['string'], [], 'crash') 119 | mkinst('rest', [None], ['collection'], [], 'get the remaining elements of a collection') 120 | 121 | mkinst('noop', ['sym'], [], [], 'do nothing. debugging tool for the compiler') 122 | mkinst('clear', [], [], [], 'flush the value stack down to one element (used in pattern matching)') 123 | mkinst('size', [], ['vector'], ['int'], 'get the size of a vector or collection') 124 | 125 | mkinst('compensate', ['inst', None], [], [], 'register a compensation') 126 | mkinst('wait-for-close', [], ['vector'], ['vector'], 'wait for the writers of a vector to exit') 127 | -------------------------------------------------------------------------------- /lib/magvm/intrinsic.py: -------------------------------------------------------------------------------- 1 | from util import as_dashed 2 | from table import Table, TableEntry 3 | from value import * 4 | from symbol import sym 5 | from debug import debug, set_debug, open_debug_file, open_shell 6 | from status import Success, Fail 7 | from env import Env 8 | from symbol import revsym 9 | 10 | from rpython.rlib.objectmodel import we_are_translated 11 | 12 | import os 13 | 14 | intrinsics = Table() 15 | 16 | ################## instruction implementations ############# 17 | def intrinsic(fn): 18 | intrinsic_name = as_dashed(fn.__name__) 19 | def wrapper(frame, args): 20 | debug(0, [':', '@!'+intrinsic.name] + [a.s() for a in args]) 21 | return fn(frame, args) 22 | 23 | # make sure the name is stored as a symbol 24 | sym(intrinsic_name) 25 | 26 | intrinsic = Intrinsic(intrinsic_name, wrapper) 27 | intrinsics.register(intrinsic) 28 | return intrinsic 29 | 30 | @intrinsic 31 | def add(frame, args): 32 | total = 0 33 | for arg in args: 34 | total += arg.as_number(frame) 35 | 36 | frame.put([Int(total)]) 37 | 38 | @intrinsic 39 | def mul(frame, args): 40 | product = 1 41 | for arg in args: 42 | product *= arg.as_number(frame) 43 | 44 | frame.put([Int(product)]) 45 | 46 | @intrinsic 47 | def get(frame, args): 48 | # we can't block, so we have to specify a place to write 49 | # the value to when it's ready. eventually this will be 50 | # a collector. 51 | frame.get(frame.env.get_output(0)) 52 | 53 | @intrinsic 54 | def take(frame, args): 55 | assert isinstance(args[0], Value) 56 | count = args[0].as_number(frame) 57 | frame.env.get_input(0).channelable.read(frame.proc, count, frame.env.get_output(0)) 58 | 59 | @intrinsic 60 | def for_(frame, args): 61 | for vec in args: 62 | if not isinstance(vec, Vector): frame.fail(tagged('not-a-vector', vec)) 63 | frame.put(vec.values) 64 | 65 | @intrinsic 66 | def stdout(frame, args): 67 | try: 68 | parent_frame = frame.proc.frames[len(frame.proc.frames)-2] 69 | except IndexError: 70 | parent_frame = frame 71 | 72 | frame.push(parent_frame.env.get_output(0)) 73 | 74 | @intrinsic 75 | def fail(frame, args): 76 | frame.set_status(Fail(args[0])) 77 | frame.proc.pop() 78 | 79 | @intrinsic 80 | def crash(frame, args): 81 | frame.crash(Fail(args[0])) 82 | 83 | @intrinsic 84 | def make_channel(frame, args): 85 | frame.put([frame.proc.machine.make_channel()]) 86 | 87 | @intrinsic 88 | def str_(frame, args): 89 | out = '' 90 | for arg in args: 91 | out += arg.s() 92 | 93 | frame.put([String(out)]) 94 | 95 | @intrinsic 96 | def eq(frame, args): 97 | if args[0].eq(args[1]): 98 | frame.set_status(Success()) 99 | else: 100 | frame.set_status(Fail(String('not-eq'))) 101 | 102 | @intrinsic 103 | def len_(frame, args): 104 | vec = args[0] 105 | if isinstance(vec, Vector): 106 | frame.put([Int(len(vec.values))]) 107 | else: 108 | frame.fail(tagged('not-a-vector', vec)) 109 | return 110 | 111 | @intrinsic 112 | def getenv(frame, args): 113 | key = args[0] 114 | assert isinstance(key, String) 115 | try: 116 | frame.put([String(os.environ[key.value])]) 117 | frame.set_status(Success()) 118 | except KeyError: 119 | frame.set_status(Fail(String('no-key'))) 120 | 121 | @intrinsic 122 | def vm_debug(frame, args): 123 | level = args[0].as_number(frame) 124 | 125 | debug(0, ['-- setting debug: ', str(level)]) 126 | set_debug(level) 127 | debug(0, ['-- done setting debug: ', str(level)]) 128 | 129 | @intrinsic 130 | def vm_debug_open(frame, args): 131 | fname = args[0].s() 132 | open_debug_file(fname) 133 | 134 | @intrinsic 135 | def has(frame, args): 136 | key = args[0].s() 137 | env = args[1] 138 | assert isinstance(env, Env) 139 | boolify(env.has(sym(key)), frame, 'no-key-'+key) 140 | 141 | def boolify(b, frame, msg): 142 | if b: 143 | frame.set_status(Success()) 144 | else: 145 | frame.set_status(Fail(String(msg))) 146 | 147 | @intrinsic 148 | def vm_debugger(frame, args): 149 | open_shell(frame, args) 150 | 151 | def own_keys(e): 152 | return map(revsym, e.dict.keys()) 153 | 154 | -------------------------------------------------------------------------------- /lib/magvm/labels.py: -------------------------------------------------------------------------------- 1 | from table import Table 2 | 3 | label_table = Table() 4 | labels_by_addr = {} 5 | 6 | def register_label(label): 7 | label_table.register(label) 8 | labels_by_addr[label.addr] = label 9 | 10 | def label_by_addr(addr): 11 | return labels_by_addr[addr].name 12 | 13 | inst_table = Table() 14 | 15 | register_inst = inst_table.register 16 | -------------------------------------------------------------------------------- /lib/magvm/load.py: -------------------------------------------------------------------------------- 1 | from table import Table, Label 2 | from inst import inst_type_table, Inst 3 | from value import * 4 | from util import map_int 5 | from debug import debug 6 | from const import const_table 7 | from labels import inst_table, label_table, register_label 8 | from symbol import symbol_table, sym, revsym 9 | from intrinsic import intrinsic, intrinsics 10 | from base import base_env 11 | from spawn import spawn 12 | from status import Fail 13 | 14 | import os 15 | 16 | from rpython.rlib.rposix import spawnv 17 | from rpython.rlib.rstruct.runpack import runpack 18 | 19 | def prefixed(p): 20 | return os.environ['MAGRITTE_PREFIX'] + p 21 | 22 | def unescape(s): 23 | return s.replace('\\n', '\n').replace('\\\\', '\\') 24 | 25 | def read_int(fd): 26 | out = runpack('i', os.read(fd, 4)) 27 | return out 28 | 29 | def read_str(fd): 30 | length = read_int(fd) 31 | return os.read(fd, length) 32 | 33 | def read_constant(fd): 34 | typechar = os.read(fd, 1) 35 | if typechar == '"': return String(read_str(fd)) 36 | if typechar == '#': return Int(read_int(fd)) 37 | assert False, 'unexpected typechar %s' % typechar 38 | 39 | def load_fd(fd): 40 | offsets = { 41 | 'const': len(const_table), 42 | 'label': len(label_table), 43 | 'inst': len(inst_table), 44 | } 45 | 46 | # constants block 47 | num_constants = read_int(fd) 48 | for _ in range(0, num_constants): 49 | const = read_constant(fd) 50 | const_table.register(const) 51 | 52 | # symbols block 53 | num_symbols = read_int(fd) 54 | symbol_translation = [0] * num_symbols 55 | for i in range(0, num_symbols): 56 | val = read_str(fd) 57 | symbol_translation[i] = sym(val) 58 | 59 | num_labels = read_int(fd) 60 | for _ in range(0, num_labels): 61 | name = read_str(fd) 62 | addr = read_int(fd) + offsets['inst'] 63 | trace = None 64 | has_trace = read_int(fd) 65 | trace = None 66 | if has_trace == 1: trace = read_str(fd) 67 | label = Label(name, addr, trace) 68 | register_label(label) 69 | 70 | num_insts = read_int(fd) 71 | for i in range(0, num_insts): 72 | command = read_str(fd) 73 | num_args = read_int(fd) 74 | raw_args = [999] * num_args 75 | for i in range(0, num_args): raw_args[i] = read_int(fd) 76 | inst_type = inst_type_table.get(command) 77 | args = inst_type.reindex(raw_args, offsets, symbol_translation) 78 | inst_table.register(Inst(inst_type.id, args)) 79 | 80 | def load_file(fname): 81 | fd = 0 82 | 83 | try: 84 | fd = os.open(fname, os.O_RDONLY, 0o777) 85 | load_fd(fd) 86 | finally: 87 | os.close(fd) 88 | 89 | label = label_table.get('main') 90 | debug(0, ['load!', fname, str(label.addr)]) 91 | return label 92 | 93 | def precompile_and_load_file(fname): 94 | precompile(fname) 95 | return load_file(fname + 'c') 96 | 97 | def decomp_to_file(fname): 98 | fd = 0 99 | 100 | try: 101 | fd = os.open(fname, os.O_WRONLY | os.O_CREAT, 0o777) 102 | decomp_fd(fd) 103 | finally: 104 | os.close(fd) 105 | 106 | def arg_as_str(inst_type, i, arg): 107 | arg_type = None 108 | try: 109 | arg_type = inst_type.static_types[i] 110 | if arg_type is None: return '#'+str(arg) 111 | if arg_type == 'inst': return '@'+labels_by_addr[arg].name 112 | if arg_type == 'const': return '+'+const_table.lookup(arg).s() 113 | if arg_type == 'sym': return ':'+revsym(arg) 114 | if arg_type == 'intrinsic': return '@!'+intrinsics.lookup(arg).name 115 | except KeyError as e: 116 | debug(0, ['no key:', inst_type.name, arg_type or '??', str(i), str(arg)]) 117 | except IndexError as e: 118 | debug(0, ['no index:', inst_type.name, arg_type or '??', str(i), str(arg)]) 119 | 120 | return '?%s' % str(arg) 121 | 122 | 123 | def decomp_fd(fd): 124 | out = [] 125 | 126 | out.append('==== symbols ====\n') 127 | for s in symbol_table.table: 128 | out.append('%d %s\n' % (s.id, s.name)) 129 | 130 | out.append('\n') 131 | out.append('==== consts ====\n') 132 | for (i, c) in enumerate(const_table.table): 133 | out.append('%d %s\n' % (i, c.s())) 134 | 135 | out.append('==== labels ====\n') 136 | for (i, l) in enumerate(label_table.table): 137 | out.append('%d %s\n' % (i, l.s())) 138 | 139 | out.append('==== instructions ====\n') 140 | for (i, inst) in enumerate(inst_table.table): 141 | try: 142 | label = labels_by_addr[i] 143 | out.append("%s:" % label.name) 144 | if label.trace: 145 | out.append(" %s" % label.trace) 146 | out.append('\n') 147 | except KeyError: 148 | pass 149 | 150 | inst_type = inst.type() 151 | typename = inst_type.name 152 | out.append(' %d %s' % (i, typename)) 153 | 154 | for (i, arg) in enumerate(inst.arguments): 155 | out.append(' %s' % arg_as_str(inst_type, i, arg)) 156 | 157 | out.append('\n') 158 | 159 | os.write(fd, ''.join(out)) 160 | 161 | def _get_fname(args): 162 | assert len(args) == 1 163 | fname_obj = args[0] 164 | assert isinstance(fname_obj, String) 165 | fname = fname_obj.value 166 | assert fname is not None 167 | return fname 168 | 169 | def precompile(fname): 170 | fnamec = fname + 'c' 171 | if os.path.exists(fnamec) and os.path.getmtime(fnamec) >= os.path.getmtime(fname): return 172 | 173 | mag_binary = prefixed('/bin/magc') 174 | 175 | debug(0, ['magc out of date, recompiling', mag_binary, fname]) 176 | spawn(mag_binary, [fname]) 177 | 178 | @intrinsic 179 | def load(frame, args): 180 | fname = _get_fname(args) 181 | 182 | if not os.path.exists(fname): fname = prefixed(fname) 183 | 184 | if not os.path.exists(fname): 185 | frame.fail(tagged('no-such-file', String(fname))) 186 | 187 | label = precompile_and_load_file(fname) 188 | 189 | frame.proc.frame(base_env, label.addr, tail_elim=False) 190 | 191 | base_env.let(sym('load'), load) 192 | 193 | @intrinsic 194 | def decomp(frame, args): 195 | if len(args) == 0: 196 | decomp_fd(1) # stdout 197 | else: 198 | fname = _get_fname(args) 199 | decomp_to_file(fname) 200 | 201 | -------------------------------------------------------------------------------- /lib/magvm/machine.py: -------------------------------------------------------------------------------- 1 | import sys # NOT RPYTHON 2 | import os 3 | from rpython.rlib.objectmodel import enforceargs 4 | import rpython.rtyper.lltypesystem.lltype as lltype 5 | from table import Table 6 | from util import as_dashed, map_int 7 | from value import * 8 | from proc import Proc, Frame 9 | from debug import debug 10 | from channel import * 11 | from base import base_env 12 | from symbol import symbol_table 13 | from labels import label_table 14 | from rpython.rlib.jit import JitDriver, elidable 15 | from rpython.rlib.listsort import make_timsort_class 16 | 17 | from random import shuffle 18 | 19 | jit_driver = JitDriver(greens=['pc'], reds=['env', 'stack']) 20 | 21 | ################## machine #################### 22 | # This class contains the main loop of the vm, 23 | # and implements basic scheduling among a 24 | # collection of Proc objects. 25 | class Machine(object): 26 | def __init__(self): 27 | self.procs = Table() 28 | self.channels = Table() 29 | 30 | def make_channel(self): 31 | return self.channels.register(Channel()) 32 | 33 | def spawn_label(self, env, label): 34 | return self.spawn(env, label_table.get(label).addr) 35 | 36 | def spawn(self, env, addr): 37 | proc = Proc(self) 38 | self.procs.register(proc) 39 | proc.frame(env, addr) 40 | return proc 41 | 42 | def run(self): 43 | debug(0, ['run!']) 44 | try: 45 | while True: self.step() 46 | except Done: 47 | return self.procs 48 | 49 | assert False # impossible 50 | 51 | def step(self): 52 | debug(0, ['%%%%% PHASE: step %%%%%']) 53 | sort = age_sort([p for p in self.procs.table]) 54 | sort.sort() 55 | for proc in sort.list: 56 | if not proc: continue 57 | if proc.state == Proc.DONE: continue 58 | 59 | debug(0, ['-- running proc', proc.s()]) 60 | if proc.is_running(): 61 | proc.age += 1 62 | 63 | jit_driver.jit_merge_point( 64 | pc=proc.current_frame().pc, 65 | stack=proc.frames, 66 | env=proc.current_frame().env 67 | ) 68 | 69 | proc.step() 70 | 71 | debug(0, ['%%%%% PHASE: resolve %%%%%']) 72 | for channel in self.channels.table: 73 | debug(0, ['+', channel.s()]) 74 | assert channel.channelable is not None 75 | channel.channelable.resolve() 76 | 77 | debug(0, ['%%%%% PHASE: check %%%%%']) 78 | for p in self.procs.table: debug(0, [p.s()]) 79 | 80 | running = 0 81 | waiting = 0 82 | for proc in self.procs.table: 83 | if proc.is_running(): 84 | running += 1 85 | elif proc.state == Proc.WAITING: 86 | waiting += 1 87 | 88 | if running == 0 and waiting > 0: raise Deadlock 89 | if running == 0: raise Done 90 | 91 | machine = Machine() 92 | 93 | age_sort = make_timsort_class(lt=lambda p, q: p.age < q.age) 94 | -------------------------------------------------------------------------------- /lib/magvm/proc.py: -------------------------------------------------------------------------------- 1 | from table import TableEntry 2 | from inst import inst_type_table, InstType 3 | from channel import Channel, Close 4 | from labels import labels_by_addr, inst_table 5 | from status import Status, Success, Fail 6 | from util import print_list_s 7 | from frame import Frame 8 | from debug import debug, open_shell 9 | from value import * 10 | 11 | ############# processes ####################### 12 | # This class represents one process, running 13 | # concurrently with others. It is ticked forward 14 | # one step at a time by the machine through the 15 | # .step() method. This method tries to evaluate 16 | # as much as it can before it is set to wait. 17 | class Proc(TableEntry): 18 | INIT = 0 # hasn't started running yet 19 | RUNNING = 1 # can safely step forward 20 | WAITING = 2 # waiting to be woken up by a channel 21 | INTERRUPTED = 3 # the waiting channel has closed, need to unwind the stack 22 | DONE = 4 # no more steps to run 23 | TERMINATED = 5 # there has been a crash, probably due to an error 24 | 25 | def set_init(self): 26 | debug(0, ['-- set-init', self.s()]) 27 | self.state = Proc.INIT 28 | 29 | def set_running(self): 30 | debug(0, ['-- set-running', self.s()]) 31 | self.state = Proc.RUNNING 32 | 33 | def set_waiting(self): 34 | debug(0, ['-- set-waiting', self.s()]) 35 | if self.state == Proc.WAITING: 36 | raise StandardError('oh no') 37 | self.state = Proc.WAITING 38 | 39 | def set_interrupted(self): 40 | debug(0, ['-- set-interrupted', self.s()]) 41 | self.state = Proc.INTERRUPTED 42 | 43 | def set_done(self): 44 | debug(0, ['-- set-done', self.s()]) 45 | self.state = Proc.DONE 46 | 47 | def set_terminated(self): 48 | debug(0, ['-- set-terminated', self.s()]) 49 | self.state = Proc.TERMINATED 50 | 51 | 52 | # Important: a channel will set a successful write to "running". but 53 | # if that write is connected to a closed channel, it needs to preserve 54 | # the INTERRUPTED state. So on a successful read or write, we only set 55 | # the state back to RUNNING if we can verify it's still WAITING. 56 | def try_set_running(self): 57 | if self.state == Proc.WAITING: 58 | debug(0, ['try_set_running: set to running', self.s()]) 59 | self.set_running() 60 | else: 61 | debug(0, ['try_set_running: not waiting', self.s()]) 62 | 63 | def state_name(self): 64 | if self.state == Proc.INIT: return 'init' 65 | if self.state == Proc.RUNNING: return 'running' 66 | if self.state == Proc.WAITING: return 'waiting' 67 | if self.state == Proc.INTERRUPTED: return 'interrupted' 68 | if self.state == Proc.DONE: return 'done' 69 | if self.state == Proc.TERMINATED: return 'terminated' 70 | assert False 71 | 72 | def is_running(self): 73 | return self.state in [Proc.INIT, Proc.RUNNING, Proc.INTERRUPTED] 74 | 75 | def __init__(self, machine): 76 | self.age = 0 77 | self.state = Proc.INIT 78 | self.machine = machine 79 | self.frames = [] 80 | self.interrupts = [] 81 | self.status = Success() 82 | self.last_cleaned = [] 83 | 84 | # Push a new frame on to the call stack, with a given environment 85 | # and starting instruction. This will automatically tail eliminate 86 | # any finished frames from the top. However, this can be disabled on 87 | # a case-by-case basis by the `tail_elim` parameter for things like 88 | # loading files, where we need to preserve the base environment. 89 | def frame(self, env, addr, tail_elim=True): 90 | debug(0, ['--', str(self.id), labels_by_addr[addr].name]) 91 | assert isinstance(addr, int) 92 | 93 | # must setup before tail eliminating so the number of registered 94 | # channels doesn't go to zero from tail elim 95 | frame = Frame(self, env, addr) 96 | frame.setup() 97 | 98 | if tail_elim: 99 | eliminated = self.tail_eliminate() 100 | 101 | # when we eliminate a frame, we need to preserve 102 | # its stack variables and compensations for the 103 | # new frame. 104 | for e in eliminated: 105 | frame.env = e.env.extend().merge(frame.env) 106 | for comp in e.compensations: 107 | frame.compensations.append(comp) 108 | 109 | self.frames.append(frame) 110 | return frame 111 | 112 | def tail_eliminate(self): 113 | out = [] 114 | 115 | # don't tail eliminate the root frame, for Reasons. 116 | # the root frame might have the global env and we really don't want 117 | # to mess with that env 118 | if len(self.frames) <= 1: return [] 119 | 120 | while len(self.frames) > 1 and self.current_frame().should_eliminate(): 121 | debug(0, ['-- tco', self.s()]) 122 | out.append(self.pop()) 123 | 124 | debug(0, ['-- post-tco', self.s()]) 125 | 126 | return out 127 | 128 | def current_frame(self): 129 | return self.frames[len(self.frames)-1] 130 | 131 | def pop(self): 132 | top = self.frames.pop() 133 | top.cleanup() 134 | return top 135 | 136 | def step(self): 137 | debug(0, ['=== step %s ===' % self.s()]) 138 | 139 | if self.frames: 140 | env = self.current_frame().env 141 | debug(0, ['env:', env.s()]) 142 | debug(0, ['in:', env.get_input(0).s() if env.get_input(0) else '_']) 143 | debug(0, ['out:', env.get_output(0).s() if env.get_output(0) else '_']) 144 | 145 | try: 146 | while self.frames and self.is_running(): 147 | # IMPORTANT: only check interrupts when we're waiting! 148 | if self.state == Proc.INTERRUPTED and self.check_interrupts(): return 149 | 150 | if self.last_cleaned: 151 | debug(0, ['-- emptying last_cleaned'] + 152 | [f.s() for f in self.last_cleaned]) 153 | 154 | self.last_cleaned = [] 155 | self.set_running() 156 | self.current_frame().step() 157 | except Crash as e: 158 | print '-- crash', e.status.s() 159 | debug(0, ['-- crashed', self.s()]) 160 | self.status = e.status 161 | self.set_terminated() 162 | 163 | if not self.frames: 164 | debug(0, ['out of frames', self.s()]) 165 | self.set_done() 166 | else: 167 | debug(0, ['still has frames!', self.s()]) 168 | 169 | 170 | def check_interrupts(self): 171 | debug(0, ['check_interrupts', self.s()]) 172 | if not self.interrupts: return False 173 | 174 | interrupt = self.interrupts.pop(0) 175 | debug(0, ['-- interrupted', self.s(), interrupt.s()]) 176 | 177 | # unwind the stack until the channel goes out of scope 178 | if isinstance(interrupt, Close): 179 | debug(0, ['channel closed', interrupt.s()]) 180 | debug(0, ['env:', self.current_frame().env.s()]) 181 | debug(0, ['has channel?', str(self.has_channel(interrupt.is_input, interrupt.channel))]) 182 | 183 | while self.frames and self.has_channel(interrupt.is_input, interrupt.channel): 184 | debug(0, ['unwind!']) 185 | self.pop() 186 | else: 187 | while self.frame: 188 | debug(0, ['unwind all!']) 189 | self.pop() 190 | 191 | for frame in self.last_cleaned: 192 | debug(0, ['-- compensating', frame.s(), str(len(frame.compensations))]) 193 | for (addr, _) in frame.compensations: 194 | debug(0, ['-- unwind-comp', frame.s(), labels_by_addr[addr].name]) 195 | self.frame(frame.env, addr) 196 | 197 | if self.frames: 198 | self.set_running() 199 | else: 200 | self.set_done() 201 | 202 | debug(0, ['unwound', self.s()] + [f.s() for f in self.last_cleaned]) 203 | 204 | return True 205 | 206 | def has_channel(self, is_input, channel): 207 | return self.current_frame().env.has_channel(is_input, channel) 208 | 209 | def interrupt(self, interrupt): 210 | debug(0, ['interrupt', self.s(), interrupt.s()]) 211 | self.interrupts.append(interrupt) 212 | self.set_interrupted() 213 | 214 | def s(self): 215 | out = ['') 221 | return ''.join(out) 222 | 223 | -------------------------------------------------------------------------------- /lib/magvm/shuffle.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prg-titech/magritte/6e75603c9eba36902b3facb89bef9a2b5848ec0d/lib/magvm/shuffle.py -------------------------------------------------------------------------------- /lib/magvm/spawn.py: -------------------------------------------------------------------------------- 1 | import os 2 | from rpython.rlib.rposix import execv 3 | from debug import debug 4 | 5 | def spawn(program, args): 6 | pid = os.fork() 7 | if pid == 0: 8 | # the forked process 9 | execv(program, [program] + args) 10 | else: 11 | os.waitpid(pid, 0) 12 | -------------------------------------------------------------------------------- /lib/magvm/status.py: -------------------------------------------------------------------------------- 1 | from value import * 2 | 3 | class Status(Value): 4 | def is_success(self): 5 | return True 6 | 7 | class Success(Status): 8 | def __init__(self): 9 | pass 10 | 11 | def s(self): 12 | return '' 13 | 14 | class Fail(Status): 15 | def __init__(self, reason): 16 | self.reason = reason 17 | 18 | def is_success(self): 19 | return False 20 | 21 | def s(self): 22 | return '' % self.reason.s() 23 | -------------------------------------------------------------------------------- /lib/magvm/symbol.py: -------------------------------------------------------------------------------- 1 | from table import Table, TableEntry 2 | 3 | #### the global symbol table #### 4 | 5 | class SymbolTable(Table): 6 | def sym(self, string): 7 | assert isinstance(string, str) 8 | try: 9 | return self.rev_table[string].id 10 | except KeyError: 11 | return self.register(Symbol(string)).id 12 | 13 | def revsym(self, idx): 14 | assert isinstance(idx, int) 15 | 16 | return self.table[idx].name 17 | 18 | class Symbol(TableEntry): 19 | def __init__(self, string): self.name = string 20 | 21 | 22 | symbol_table = SymbolTable() 23 | sym = symbol_table.sym 24 | revsym = symbol_table.revsym 25 | -------------------------------------------------------------------------------- /lib/magvm/table.py: -------------------------------------------------------------------------------- 1 | class Table(object): 2 | def __init__(self): 3 | self.table = [] 4 | self.rev_table = {} 5 | 6 | def register(self, entry): 7 | assert isinstance(entry, TableEntry) 8 | entry.id = len(self) 9 | self.table.append(entry) 10 | if entry.name: 11 | self.rev_table[entry.name] = entry 12 | return entry 13 | 14 | def get(self, name): 15 | return self.rev_table[name] 16 | 17 | def __len__(self): 18 | return len(self.table) 19 | 20 | def lookup(self, idx): 21 | try: 22 | return self.table[idx] 23 | except IndexError: 24 | print 'no index', idx 25 | raise 26 | 27 | class TableEntry(object): 28 | id = -1 29 | name = None 30 | 31 | def __init__(*a): 32 | raise NotImplementedError 33 | 34 | def s(self): 35 | raise NotImplementedError 36 | return '' # must be a string 37 | 38 | class Label(TableEntry): 39 | def __init__(self, name, addr, trace): 40 | assert isinstance(addr, int) 41 | self.name = name 42 | self.addr = addr 43 | self.trace = trace 44 | 45 | def s(self): 46 | if self.trace: 47 | return '%s@%s' % (self.name, self.trace) 48 | else: 49 | return self.name 50 | 51 | -------------------------------------------------------------------------------- /lib/magvm/targetmagritte.py: -------------------------------------------------------------------------------- 1 | from machine import machine 2 | from base import base_env 3 | from load import load_file, precompile_and_load_file, prefixed 4 | from debug import debugger, debug 5 | from rpython.rlib.objectmodel import we_are_translated 6 | import os 7 | 8 | ####### main target ########################## 9 | # This is the main target file for compilation by RPython. 10 | # The whole program will start at the `entry_point` function. 11 | 12 | def run_file(filename): 13 | load_file(filename) 14 | machine.spawn_label(base_env, 'main') 15 | machine.run() 16 | return 0 17 | 18 | def run_prelude(): 19 | main = precompile_and_load_file(prefixed('/lib/mag/prelude.mag')) 20 | machine.spawn(base_env, main.addr) 21 | 22 | def usage(): 23 | print "TODO: usage" 24 | 25 | def entry_point(argv): 26 | debugger.setup() 27 | if we_are_translated(): 28 | debug(0, ['== starting magvm in native mode ==']) 29 | else: 30 | debug(0, ['== starting magvm in interpreted mode ==']) 31 | 32 | filename = None 33 | 34 | while argv: 35 | arg = argv.pop(0) 36 | if arg == '-f': 37 | filename = argv.pop(0) 38 | elif arg == '-h': 39 | usage() 40 | else: 41 | filename = arg 42 | 43 | if filename is None: 44 | usage() 45 | return 1 46 | 47 | run_prelude() 48 | return run_file(filename) 49 | 50 | def target(*args): 51 | return entry_point 52 | 53 | if __name__ == '__main__': 54 | import sys 55 | entry_point(sys.argv) 56 | -------------------------------------------------------------------------------- /lib/magvm/targettest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from rpython.rlib.rposix import execv 3 | 4 | def spawn(program, args): 5 | pid = os.fork() 6 | if pid == 0: 7 | # the forked process 8 | execv(program, [program] + args) 9 | else: 10 | os.waitpid(pid, 0) 11 | 12 | def entry_point(argv): 13 | spawn('/usr/bin/echo', ['hello', 'world']) 14 | print 'done!' 15 | return 0 16 | 17 | def target(*args): 18 | return entry_point 19 | -------------------------------------------------------------------------------- /lib/magvm/util.py: -------------------------------------------------------------------------------- 1 | def print_list_s(tag, vals): 2 | print tag, 3 | for v in vals: print v.s(), 4 | print 5 | 6 | 7 | def as_dashed(name): 8 | name = name.replace('_', '-') 9 | 10 | # for things like `return` which can't be method names in python 11 | if name[-1] == '-': return name[:-1] 12 | else: return name 13 | 14 | # normal map isn't rpython 15 | def map(fn, arr): 16 | out = [None] * len(arr) 17 | for (i, e) in enumerate(arr): 18 | out[i] = fn(e) 19 | return out 20 | 21 | def map_int(arr): 22 | out = [0] * len(arr) 23 | for (i, e) in enumerate(arr): 24 | out[i] = int(e) 25 | return out 26 | 27 | -------------------------------------------------------------------------------- /lib/magvm/value.py: -------------------------------------------------------------------------------- 1 | from table import TableEntry 2 | from symbol import sym 3 | from debug import debug 4 | from labels import labels_by_addr 5 | 6 | from rpython.rlib.rarithmetic import r_uint, intmask 7 | 8 | ######### values! ########################### 9 | # These classes implement the core values of 10 | # the VM. They can implement a number of interfaces 11 | # through the @impl function - attached objects 12 | # that implement certain features. For example, 13 | # many values can be invoked as functions, and 14 | # the behaviour is implemented in value.invokable. 15 | # This allows us to keep the top-level shared 16 | # namespace thin. 17 | # 18 | # Since RPython's support for __repr__ can be 19 | # brittle, we equip each class with a method 20 | # .s() to retrieve the string representation. 21 | # 22 | # see channel.py for the Channel value. 23 | 24 | class Done(Exception): pass 25 | class Deadlock(Exception): pass 26 | 27 | def tagged(tag, *values): 28 | values = list(values) 29 | values.insert(0, String(tag)) 30 | return Vector(values) 31 | 32 | class Crash(Exception): 33 | def __init__(self, status): 34 | self.status = status 35 | 36 | def impl(klass): 37 | """NOT_RPYTHON""" 38 | import copy 39 | 40 | 41 | def init_fn(self, value): self._ = value 42 | 43 | def hack_method(name, method, impl_name): 44 | setattr(klass, impl_name, staticmethod(method)) 45 | setattr(klass, name, lambda self, *a: getattr(self, impl_name)(self._, *a)) 46 | 47 | for (name, method) in dict(klass.__dict__).iteritems(): 48 | if name[0] == '_': continue 49 | hack_method(name, method, '_impl_'+name) 50 | 51 | setattr(klass, '__init__', init_fn) 52 | 53 | return klass 54 | 55 | class Invokable(object): 56 | def invoke(self, frame, arguments): raise NotImplementedError 57 | 58 | class Channelable(object): 59 | def write_all(self, proc, vals): raise NotImplementedError 60 | def read(self, proc, count, into): raise NotImplementedError 61 | def write(self, proc, val): return self.write_all(proc, [val]) 62 | 63 | def resolve(self): return False 64 | def add_writer(self, frame): pass 65 | def add_reader(self, frame): pass 66 | def rm_writer(self, frame): pass 67 | def rm_reader(self, frame): pass 68 | 69 | class Value(TableEntry): 70 | name = None 71 | invokable = None 72 | channelable = None 73 | 74 | def as_number(self, frame): 75 | frame.fail(tagged('not a number', self)) 76 | 77 | def eq(self, other): 78 | # TODO 79 | return self.s() == other.s() 80 | 81 | def s(self): 82 | raise NotImplementedError 83 | 84 | def typeof(self): 85 | raise NotImplementedError 86 | 87 | class String(Value): 88 | @impl 89 | class Invoke(Invokable): 90 | def invoke(self, frame, args): 91 | invokee = None 92 | symbol = sym(self.value) 93 | try: 94 | invokee = frame.env.get(symbol) 95 | except KeyError: 96 | raise frame.fail(tagged('no-such-function', self)) 97 | 98 | if invokee.invokable: 99 | invokee.invokable.invoke(frame, args) 100 | else: 101 | frame.fail(tagged('not-invokable', invokee)) 102 | 103 | def __init__(self, string): 104 | assert isinstance(string, str) 105 | assert string is not None 106 | self.value = string 107 | self.invokable = String.Invoke(self) 108 | 109 | def s(self): return self.value 110 | def typeof(self): return 'string' 111 | 112 | def as_number(self, frame): 113 | try: 114 | return int(self.value) 115 | except ValueError: 116 | frame.fail(tagged('not-a-number', self)) 117 | 118 | class Int(Value): 119 | def __init__(self, value): 120 | assert isinstance(value, int) 121 | self.value = value 122 | 123 | def as_number(self, frame): 124 | return self.value 125 | 126 | def s(self): return str(self.value) 127 | def typeof(self): return 'int' 128 | 129 | class Collection(Value): 130 | def s(self): 131 | out = ['') 136 | 137 | return ''.join(out) 138 | 139 | def typeof(self): return 'collection' 140 | 141 | class Vector(Value): 142 | @impl 143 | class Invoke(Invokable): 144 | def invoke(self, frame, args): 145 | values = self.values 146 | 147 | if len(values) == 0: 148 | frame.fail_str('empty invoke') 149 | 150 | invokable = values[0].invokable 151 | if not invokable: frame.fail(tagged('bad-invoke', values[0])) 152 | 153 | new_args = Vector(self.values[1:] + args.values) 154 | invokable.invoke(frame, new_args) 155 | 156 | @impl 157 | class Channel(Channelable): 158 | def write_all(self, proc, values): self.push_all(values) 159 | 160 | def add_writer(self, proc): 161 | debug(0, ['-- vec add_writer', 162 | str(self.writer_count + 1), 163 | self.s()]) 164 | self.writer_count += 1 165 | 166 | def rm_writer(self, proc): 167 | debug(0, ['-- vec rm_writer', 168 | str(self.writer_count - 1), 169 | self.s()]) 170 | self.writer_count -= 1 171 | # if self.writer_count == 0: 172 | # self.is_closed = True 173 | # debug(0, ['-- vec close!']) 174 | 175 | def resolve(self): 176 | debug(0, ['-- vec resolve', str(self.writer_count), str(len(self.close_waiters or [])), self.s()]) 177 | if self.writer_count > 0: return False 178 | if self.is_closed: return False 179 | 180 | debug(0, ['-- vec close!']) 181 | self.is_closed = True 182 | 183 | if not self.close_waiters: return False 184 | for proc in self.close_waiters: 185 | proc.try_set_running() 186 | 187 | return True 188 | 189 | def __init__(self, values): 190 | self.values = values 191 | self.invokable = Vector.Invoke(self) 192 | self.channelable = Vector.Channel(self) 193 | self.close_waiters = None 194 | self.is_closed = False 195 | self.writer_count = 0 196 | 197 | def wait_for_close(self, proc): 198 | debug(0, ['-- is_closed', str(self.is_closed)]) 199 | if self.is_closed: return 200 | 201 | if self.close_waiters: self.close_waiters.append(proc) 202 | else: self.close_waiters = [proc] 203 | 204 | proc.set_waiting() 205 | 206 | def push(self, value): 207 | assert value is not None 208 | self.values.append(value) 209 | 210 | def push_all(self, values): 211 | for v in values: self.push(v) 212 | 213 | def s(self): 214 | out = ['['] 215 | not_first = False 216 | for val in self.values: 217 | if not_first: out.append(' ') 218 | not_first = True 219 | 220 | out.append(val.s()) 221 | 222 | out.append(']') 223 | return ''.join(out) 224 | 225 | def typeof(self): return 'vector' 226 | 227 | class Ref(Value): 228 | def __init__(self, value): 229 | assert value is not None 230 | self.value = value 231 | 232 | def ref_get(self): return self.value 233 | def ref_set(self, value): 234 | assert value is not None 235 | self.value = value 236 | 237 | def s(self): 238 | return '' % self.value.s() 239 | 240 | def typeof(self): return 'ref' 241 | 242 | class Placeholder(Value): 243 | def __init__(self): 244 | pass 245 | 246 | def s(self): 247 | return '' 248 | 249 | placeholder = Placeholder() 250 | 251 | class Function(Value): 252 | @impl 253 | class Invoke(Invokable): 254 | def invoke(self, frame, collection): 255 | debug(0, ['()', self.label().s()]) 256 | new_env = frame.env.extend().merge(self.env) 257 | new_frame = frame.proc.frame(new_env, self.addr) 258 | new_frame.push(collection) 259 | 260 | def __init__(self, env, addr): 261 | self.env = env 262 | self.addr = addr 263 | self.invokable = Function.Invoke(self) 264 | 265 | def label(self): 266 | return labels_by_addr[self.addr] 267 | 268 | def s(self): 269 | return '' 270 | 271 | def typeof(self): return 'function' 272 | 273 | class Intrinsic(Value): 274 | @impl 275 | class Invoke(Invokable): 276 | def invoke(self, frame, collection): 277 | self.fn(frame, collection.values) 278 | 279 | def __init__(self, name, fn): 280 | self.name = name 281 | self.fn = fn 282 | self.invokable = Intrinsic.Invoke(self) 283 | 284 | def s(self): 285 | return '@!'+self.name 286 | 287 | def typeof(self): return 'intrinsic' 288 | 289 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prg-titech/magritte/6e75603c9eba36902b3facb89bef9a2b5848ec0d/log/.gitkeep -------------------------------------------------------------------------------- /notes/guarantees: -------------------------------------------------------------------------------- 1 | === basic operations 2 | add_reader(c, p) 3 | add_writer(c, p) 4 | rm_reader(c, p) 5 | rm_writer(c, p) 6 | read(c, p) -> val 7 | write(c, p, val) 8 | 9 | === interactions 10 | * after rm_reader/rm_writer, if readers or writers set is empty, close(c) is called. any process blocked on the channel is interrupted, and channel enters closed state 11 | (note: the blocked set is a *subset* of the readers/writers set. processes that are registered but not blocked are not interrupted.) 12 | * read/write on a closed channel always results in an immediate interrupt of the process. 13 | * a process only calls add_reader/add_writer at the start, and may only directly write to those channels. (though it may spawn other processes that write to other channels) 14 | * on interrupt or return, a process always calls rm_reader/rm_writer on all its open channels (the same set as add_reader/add_writer). 15 | 16 | in particular: 17 | * no process should ever be blocked on a closed channel 18 | * no process should read or write without being registered as reader/writer 19 | * no finished or interrupted process should ever be registered as reader/writer 20 | -------------------------------------------------------------------------------- /notes/simple.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'set' 3 | 4 | $exception = StandardError.new 5 | 6 | # $... means global variable 7 | 8 | $mutex = Mutex.new 9 | $readers = Set.new 10 | $writers = Set.new 11 | $block_type = :none 12 | $open = true 13 | 14 | # an element of the block set will look like: 15 | # [thread, value] 16 | # where value is either the value to be written (if writers are blocked) 17 | # or a ref to receive the value (if readers are blocked) 18 | $block_set = [] 19 | 20 | 21 | $output_mutex = Mutex.new 22 | $output = [] 23 | 24 | def add_reader(t) 25 | $mutex.synchronize { $readers << t } 26 | end 27 | 28 | def add_writer(t) 29 | $mutex.synchronize { $writers << t } 30 | end 31 | 32 | def rm_reader(t) 33 | to_close = $mutex.synchronize do 34 | if $block_type == :read 35 | $block_set.reject! { |b| b[0] == t } 36 | end 37 | 38 | cleanup_from(t, $readers) 39 | end 40 | 41 | to_close.each { |thread| thread.raise($exception) } 42 | end 43 | 44 | def rm_writer(t) 45 | to_close = $mutex.synchronize do 46 | if $block_type == :write 47 | $block_set.reject! { |b| b[0] == t } 48 | end 49 | 50 | cleanup_from(t, $writers) 51 | end 52 | 53 | to_close.each { |thread| thread.raise($exception) } 54 | end 55 | 56 | def cleanup_from(t, set) 57 | if not $open 58 | # no threads to clean up 59 | return Set.new 60 | end 61 | 62 | set.delete(t) 63 | 64 | # if the registered set becomes empty, 65 | # close the channel. 66 | if set.empty? 67 | $open = false 68 | to_clean = $block_set.dup 69 | $block_set.clear 70 | return to_clean.map { |b| b[0] } 71 | else 72 | return Set.new 73 | end 74 | end 75 | 76 | def read(t) 77 | $mutex.synchronize do 78 | if not $open 79 | t.raise($exception) 80 | end 81 | 82 | case $block_type 83 | when :none, :read 84 | # in this case there are no waiting write threads, 85 | # so this process must block. 86 | $block_type = :read 87 | ref = [t, nil] 88 | $block_set << ref 89 | 90 | # this releases the mutex and sleeps in one atomic action. 91 | # on wakeup, it will acquire the mutex again (and immediately 92 | # release it at the end of the synchronize block). 93 | $mutex.sleep 94 | 95 | return ref[1] 96 | when :write 97 | # in this case there are waiting write threads, 98 | # so we do *not* block, and instead wake up a 99 | # write thread. 100 | write_thread, written_value = $block_set.shift 101 | 102 | if $block_set.empty? 103 | $block_type = :none 104 | end 105 | 106 | write_thread.run 107 | 108 | return written_value 109 | end 110 | end 111 | end 112 | 113 | def write(t, val) 114 | $mutex.synchronize do 115 | if not $open 116 | t.raise($exception) 117 | end 118 | 119 | case $block_type 120 | when :none, :write 121 | $block_type = :write 122 | $block_set << [t, val] 123 | $mutex.sleep 124 | when :read 125 | read_ref = $block_set.shift 126 | 127 | if $block_set.empty? 128 | $block_type = :none 129 | end 130 | 131 | # here we mutate the ref from the block_set 132 | # before waking it up again, so that it can receive the 133 | # value when it wakes up. we guarantee that at the end 134 | # of the synchronize block in #read, this value is set. 135 | read_ref[1] = val 136 | 137 | read_ref[0].run 138 | end 139 | end 140 | end 141 | 142 | def spawn_producer!(init=0, step=1) 143 | Thread.new do 144 | begin 145 | add_writer(Thread.current) 146 | i = init 147 | loop do 148 | write(Thread.current, i) 149 | i += step 150 | end 151 | ensure 152 | rm_writer(Thread.current) 153 | end 154 | end 155 | end 156 | 157 | def spawn_consumer! 158 | Thread.new do 159 | begin 160 | add_reader(Thread.current) 161 | 10.times do 162 | # yield the thread randomly to increase the chance of races 163 | sleep(rand / 10) 164 | $output_mutex.synchronize { $output << read(Thread.current) } 165 | end 166 | ensure 167 | rm_reader(Thread.current) 168 | end 169 | end 170 | end 171 | 172 | # produce all even numbers 173 | producer1 = spawn_producer!(0, 2) 174 | 175 | # produce all odd numbers 176 | producer2 = spawn_producer!(1, 2) 177 | 178 | consumer1 = spawn_consumer! 179 | consumer2 = spawn_consumer! 180 | consumer3 = spawn_consumer! 181 | 182 | consumer1.join 183 | consumer2.join 184 | consumer3.join 185 | 186 | # the "rescue"s are necessary here because .join will re-raise the Inter 187 | # exception in our thread 188 | producer1.join rescue nil 189 | producer2.join rescue nil 190 | 191 | puts "output: #{$output.inspect}" 192 | puts "live threads (should be exactly 1, in \"run\" state):" 193 | puts Thread.list.map(&:inspect) 194 | -------------------------------------------------------------------------------- /old-spec/channel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | describe Magritte::Channel do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /old-spec/code_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Code do 2 | def with_no_dangling_threads(&b) 3 | orig_threads = Thread.list 4 | out = yield 5 | 6 | begin 7 | dangling = Thread.list - orig_threads 8 | 9 | assert { dangling.empty? } 10 | out 11 | rescue Minitest::Assertion 12 | retry_count ||= 0 13 | retry_count += 1 14 | raise if retry_count > 20 15 | 16 | # yield the current thread to allow other threads to be cleaned up 17 | # since sometimes it takes a bit of time for thread.raise to actually 18 | # kill the thread and there's no way to wait for it :\ 19 | sleep 0.1 20 | 21 | retry 22 | end 23 | end 24 | 25 | let(:output) { with_no_dangling_threads { code.spawn_collect } } 26 | 27 | abstract(:code) 28 | def self.code(&b) 29 | let(:code) { Magritte::Code.new { Magritte::Code::DSL.instance_eval(&b) } } 30 | end 31 | 32 | after { Magritte::PRINTER.p output: output } 33 | 34 | describe 'a single pipe' do 35 | # ( put 1 | put (5 + (get)) ); put 10 36 | code do 37 | s { put 1 }.p { put(5 + get) }.call 38 | put 10 39 | end 40 | 41 | it 'successfully pipes' do 42 | assert { output == [6, 10] } 43 | end 44 | end 45 | 46 | describe 'multiple pipes' do 47 | # (put 1; put 2) | add (get) (get) | mul 2 (get) 48 | code do 49 | s { put 1; put 2 }.p { put(get + get) }.p { put(get * 2) }.call 50 | 51 | put 10 52 | end 53 | 54 | it 'successfully pipes' do 55 | assert { output == [6, 10] } 56 | end 57 | end 58 | 59 | describe 'cleanup write end' do 60 | # count-from 0 | take 3 61 | code do 62 | s { for_ (0..Infinity) } 63 | .p { put get; put get; put get }.call 64 | end 65 | 66 | it 'returns normally' do 67 | assert { output == [0, 1, 2] } 68 | end 69 | end 70 | 71 | describe 'cleanup read end' do 72 | # for [0 1 2 3] | map [mul 2] 73 | code do 74 | s { for_ (0..3) } 75 | .p { map { |x| x * 2 } }.call 76 | 77 | put 10 78 | end 79 | 80 | it 'returns normally' do 81 | assert { output == [0, 2, 4, 6, 10] } 82 | end 83 | end 84 | 85 | describe 'regular looping' do 86 | code do 87 | s { for_ (0..3) } 88 | .p { loop { put (get * 2) } }.call 89 | 90 | raise "never reached" 91 | end 92 | 93 | it 'interrupts the parent process' do 94 | assert { output == [0, 2, 4, 6] } 95 | end 96 | end 97 | 98 | describe 'multiple writers' do 99 | code do 100 | # c = (make-channel) 101 | # & (put 2; put 4; put 6) > $c 102 | # & (put 1; put 3; put 5) > $c 103 | # drain < $c 104 | c = Magritte::Channel.new 105 | s { put 2; put 4; put 6 }.into(c).go 106 | s { put 1; put 3; put 5 }.into(c).go 107 | 108 | s { drain }.from(c).call 109 | end 110 | 111 | it 'combines the outputs' do 112 | assert { output.size == 6 } 113 | assert { output.select(&:even?) == [2, 4, 6] } 114 | assert { output.select(&:odd?) == [1, 3, 5] } 115 | end 116 | end 117 | 118 | describe 'multiple writers to a Collector' do 119 | code do 120 | p1 = s { put 2; put 4; put 6 }.go 121 | p2 = s { put 1; put 3; put 5 }.go 122 | 123 | p1.join 124 | p2.join 125 | end 126 | 127 | it 'combines the outputs' do 128 | assert { output.size == 6 } 129 | assert { output.select(&:even?) == [2, 4, 6] } 130 | assert { output.select(&:odd?) == [1, 3, 5] } 131 | end 132 | end 133 | 134 | describe 'multiple readers' do 135 | code do 136 | c = make_channel 137 | 138 | s { 139 | s { for_ (0..Infinity) }.into(c).go 140 | s { drain }.from(c).go 141 | s { drain }.from(c).call 142 | }.p { take 10 }.call 143 | end 144 | 145 | it 'does a thing' do 146 | assert { output.size == 10 } 147 | 148 | # no duplicates! 149 | assert { output.uniq.size == 10 } 150 | 151 | # there is a possibility that the 11th read from the counter 152 | # will beat the nth read from the counter to write into 153 | # the `take 10` process, in which case the number 10 will 154 | # replace the number n 155 | assert { ((0..10).to_a - output).size == 1 } 156 | end 157 | end 158 | 159 | describe 'multi pipe' do 160 | code do 161 | s { for_ (0...30) }.p { 162 | s { drain }.go 163 | s { drain }.go 164 | s { drain }.go 165 | }.p { take 30 }.call 166 | end 167 | 168 | it 'collects the outputs' do 169 | assert { output.sort == (0...30).to_a } 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /old-spec/env_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Env do 2 | def env(parent, vals={}) 3 | e = Magritte::Env.new(parent) 4 | 5 | vals.each do |key, val| 6 | e.let(key, val) 7 | end 8 | e 9 | end 10 | 11 | let(:base) { Magritte::Env.empty } 12 | 13 | describe "empty" do 14 | it "raises missing on get" do 15 | assert { rescuing(Magritte::Env::MissingVariable) { base.get(:foo) } } 16 | end 17 | end 18 | 19 | let(:extended) { env(base, foo: 1) } 20 | 21 | describe "own keys" do 22 | it "gets" do 23 | assert { extended.get(:foo) == 1 } 24 | end 25 | 26 | it "mutates" do 27 | extended.mut(:foo, 2) 28 | assert { extended.get(:foo) == 2 } 29 | end 30 | 31 | it "shadows" do 32 | extended.let(:foo, 2) 33 | assert { extended.get(:foo) == 2 } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /old-spec/free_vars_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::FreeVars do 2 | abstract(:input) 3 | let(:lex) { Magritte::Lexer.new("test",input) } 4 | let(:skel) { Magritte::Skeleton::Parser.parse(lex) } 5 | let(:ast) { Magritte::Parser.parse_root(skel) } 6 | let(:result) { Magritte::FreeVars.scan(ast) } 7 | 8 | def free_vars(node = nil) 9 | node ||= ast 10 | result[node] 11 | end 12 | 13 | describe "a variable" do 14 | let(:input) { 15 | """ 16 | $foo 17 | """ 18 | } 19 | 20 | it "has no free variables" do 21 | assert { free_vars.empty? } 22 | end 23 | end 24 | 25 | describe "a lexical variable" do 26 | let(:input) { 27 | """ 28 | %foo 29 | """ 30 | } 31 | 32 | it "has a free variable" do 33 | assert { free_vars == Set.new(["foo"]) } 34 | end 35 | end 36 | 37 | describe "a binder" do 38 | let(:input) { 39 | """ 40 | ?x 41 | """ 42 | } 43 | 44 | it "has no free variables" do 45 | assert { free_vars.empty? } 46 | end 47 | end 48 | 49 | describe "a lambda" do 50 | describe "has a free variable" do 51 | let(:input) { 52 | """ 53 | (x ?foo) = $hoge %bar 54 | """ 55 | } 56 | 57 | it "has a free variables" do 58 | assert { free_vars == Set.new(["bar"]) } 59 | end 60 | end 61 | 62 | describe "has no free variable" do 63 | let(:input) { 64 | """ 65 | (x ?foo) = $hoge %foo 66 | """ 67 | } 68 | 69 | it "has no free variables" do 70 | assert { free_vars.empty? } 71 | end 72 | end 73 | end 74 | 75 | describe "a pipe" do 76 | let(:input) { 77 | """ 78 | ?in | ?out 79 | """ 80 | } 81 | 82 | it "has no free variables" do 83 | assert { free_vars.empty? } 84 | end 85 | end 86 | 87 | #describe "a compensation" do 88 | # let(:input) { 89 | # """ 90 | # %command %% %reset 91 | # """ 92 | # } 93 | 94 | # it "has a free variable" do 95 | # assert { free_vars == Set.new(["hoge","bar"]) } 96 | # end 97 | #end 98 | 99 | describe "a spawn" do 100 | let(:input) { 101 | """ 102 | & $x %foo 103 | """ 104 | } 105 | 106 | it "has a free variable" do 107 | assert { free_vars == Set.new(["foo"]) } 108 | end 109 | end 110 | 111 | describe "a command" do 112 | let(:input) { 113 | """ 114 | %foo $bar 115 | """ 116 | } 117 | 118 | it "has a free variable" do 119 | assert { free_vars == Set.new(["foo"]) } 120 | end 121 | end 122 | 123 | describe "a block" do 124 | let(:input) { 125 | """ 126 | (foo %hoge) 127 | (zoo $bar) 128 | """ 129 | } 130 | 131 | it "has a free variable" do 132 | assert { free_vars == Set.new(["hoge"]) } 133 | end 134 | end 135 | 136 | describe "a vector" do 137 | let(:input) { 138 | """ 139 | [$a %b] 140 | """ 141 | } 142 | 143 | it "has a free variable" do 144 | assert { free_vars == Set.new(["b"]) } 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /old-spec/interpret/basic_spec.rb: -------------------------------------------------------------------------------- 1 | f.interpret_spec "basic functionality" do 2 | interpret "a vector" do 3 | source <<-EOF 4 | put [a b c] 5 | EOF 6 | 7 | result "[a b c]" 8 | end 9 | 10 | interpret "single word" do 11 | source <<-EOF 12 | put hello 13 | EOF 14 | 15 | result "hello" 16 | end 17 | 18 | interpret "nested expression" do 19 | source <<-EOF 20 | put (put 1) 21 | EOF 22 | 23 | result "1" 24 | end 25 | 26 | interpret "early exist for collectors" do 27 | source <<-EOF 28 | put 1 2 3 4 5 6 7 8 9 10 | (& drain; & drain) 29 | EOF 30 | 31 | results_size 10 32 | end 33 | 34 | interpret "early exit for vectors" do 35 | source <<-EOF 36 | for [0 (put 1 2 3 4 5 6 7 8 9 10 | (& drain; & drain))] 37 | EOF 38 | 39 | results_size 11 40 | end 41 | 42 | interpret "slide example" do 43 | source <<-EOF 44 | count-forever | (& drain; & drain; & drain) | take 30 45 | EOF 46 | 47 | results_size 30 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /old-spec/interpret/block_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "block and other grouping constructs" do 2 | interpret "block scoping" do 3 | source <<-EOF 4 | x = 1 5 | put (x = 2; put $x) 6 | (x = 3; put $x) 7 | put $x 8 | EOF 9 | 10 | results %w(2 3 1) 11 | end 12 | 13 | interpret "root-level blocks" do 14 | source <<-EOF 15 | (put 1) 16 | EOF 17 | 18 | result "1" 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /old-spec/interpret/cond_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "conditionals" do 2 | interpret "simple" do 3 | source <<-EOF 4 | true && put 1 5 | false || put 2 6 | true || put 3 7 | false && put 4 8 | true 9 | EOF 10 | 11 | results %w(1 2) 12 | end 13 | 14 | interpret "else" do 15 | source <<-EOF 16 | true && put 1 !! put 2 17 | false && put 3 !! put 4 18 | true || put 5 !! put 6 19 | false || put 7 !! put 8 20 | EOF 21 | 22 | results %w(1 4 6 7) 23 | end 24 | 25 | interpret "try" do 26 | source <<-EOF 27 | try crash && put success !! put crashed 28 | EOF 29 | 30 | result "crashed" 31 | end 32 | 33 | interpret "equality" do 34 | source <<-EOF 35 | eq 10 10 || put fail1 36 | eq 10 11 && put fail2 37 | eq foo foo || put fail3 38 | eq foo bar && put fail4 39 | eq [1 2] [1 (add 1 1)] || put fail5 40 | eq [1 2] [1 3] && put fail6 41 | eq [1 2] [1 2 3] && put fail7 42 | eq [1 2 3] [1 2] && put fail8 43 | eq [1 [2 3]] [1 [2 3]] || put fail9 44 | c = (make-channel) 45 | eq $c $c || put fail10 46 | eq $c (make-channel) && put fail11 47 | eq [1] 1 && put fail12 48 | true 49 | EOF 50 | 51 | results [] 52 | end 53 | 54 | interpret "returning booleans" do 55 | source <<-EOF 56 | (f ?x) = (true) 57 | f 10 && put success 58 | EOF 59 | 60 | result 'success' 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /old-spec/interpret/env_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "environment" do 2 | interpret "creation" do 3 | source <<-EOF 4 | e = {x = 1; y = 0} 5 | put %e!x %e!y 6 | EOF 7 | 8 | results %w(1 0) 9 | end 10 | 11 | interpret "complex creation" do 12 | source <<-EOF 13 | x = 1 14 | y = 0 15 | e = {z = $x; k = $y} 16 | put $e!z $e!k 17 | EOF 18 | 19 | results %w(1 0) 20 | end 21 | 22 | interpret "nesting" do 23 | source <<-EOF 24 | e = {g = 3} 25 | e2 = {h = 4} 26 | e3 = {x = $e; y = $e2} 27 | put $e3!x!g $e3!y!h 28 | EOF 29 | 30 | results %w(3 4) 31 | end 32 | 33 | interpret "printing" do 34 | source <<-EOF 35 | e = {g = 3; x = 1} 36 | put $e 37 | EOF 38 | 39 | result "{ g = 3; x = 1 }" 40 | end 41 | 42 | interpret "environment functions" do 43 | source <<-EOF 44 | e = { x = 2; (f ?y) = (put %x %y) } 45 | $e!f 1 46 | EOF 47 | 48 | results %w(2 1) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /old-spec/interpret/example_spec.rb: -------------------------------------------------------------------------------- 1 | # interpret_spec "basic functionality" do 2 | # interpret "basic.mag" do 3 | # source <<-EOF 4 | # example = (load example/basic.mag) 5 | # %example!f 6 | # EOF 7 | # 8 | # results %w(10) 9 | # end 10 | # 11 | # interpret "server.mag" do 12 | # source <<-EOF 13 | # example = (load example/server.mag) 14 | # %example!__main__ 15 | # EOF 16 | # 17 | # results ["hello world"] 18 | # end 19 | # end 20 | -------------------------------------------------------------------------------- /old-spec/interpret/interrupt_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "compensations" do 2 | interpret "unconditional checkpoint" do 3 | source <<-EOF 4 | exec (=> 5 | put 1 %%! put 2 6 | put 3 7 | ) 8 | EOF 9 | 10 | results %w(1 3 2) 11 | end 12 | 13 | interpret "interrupts" do 14 | source <<-EOF 15 | c = (make-channel) 16 | exec (=> ( 17 | put 1 %% (put comp > %c) 18 | put 2 3 4 5 6 19 | )) | take 2 20 | get < $c 21 | EOF 22 | 23 | results %w(1 2 comp) 24 | end 25 | 26 | interpret "interrupts on redirections" do 27 | source <<-EOF 28 | c = (make-channel) 29 | & put 1 2 3 > $c 30 | drain < $c 31 | (dr) = (put (get); dr) 32 | dr < $c 33 | 34 | # we never get here, because we get 35 | # interrupted by the above 36 | put 4 37 | EOF 38 | 39 | results %w(1 2 3) 40 | end 41 | 42 | interpret "get masking" do 43 | source <<-EOF 44 | c = (make-channel) 45 | & put 1 > $c 46 | get < $c 47 | get < $c 48 | 49 | # we never get here, because we get interrupted 50 | # by the above 51 | put 4 52 | EOF 53 | 54 | results %w(1) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /old-spec/interpret/lambda_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "lambda functionality" do 2 | interpret "call" do 3 | source <<-EOF 4 | (?x => put $x) 1 5 | EOF 6 | 7 | result "1" 8 | end 9 | 10 | interpret "zero argument lambdas" do 11 | source <<-EOF 12 | exec (=> put 1) 13 | EOF 14 | 15 | result "1" 16 | end 17 | 18 | interpret "reducing argument length patterns" do 19 | source <<-EOF 20 | f = ( 21 | ?x ?y => put [two %x %y] 22 | ?x => put [one %x] 23 | ) 24 | 25 | f 3 26 | f 5 6 27 | EOF 28 | 29 | results ["[one 3]", "[two 5 6]"] 30 | end 31 | 32 | interpret "increasing argument length patterns" do 33 | source <<-EOF 34 | f = ( 35 | ?x => put [one %x] 36 | ?x ?y => put [two %x %y] 37 | ) 38 | 39 | f 3 40 | f 5 6 41 | EOF 42 | 43 | results ["[one 3]", "[two 5 6]"] 44 | end 45 | 46 | interpret "special syntax assignment" do 47 | source <<-EOF 48 | (f ?x) = put $x 49 | f 5 50 | put $f 51 | EOF 52 | 53 | results ["5",""] 54 | end 55 | 56 | interpret "special syntax assignment with dynamic var" do 57 | source <<-EOF 58 | f = 1 59 | ($f ?x) = put $x 60 | f 5 61 | put $f 62 | EOF 63 | 64 | results ["5",""] 65 | end 66 | 67 | interpret "special syntax assignment with lexical var" do 68 | source <<-EOF 69 | f = 1 70 | (%f ?x) = put $x 71 | f 5 72 | put $f 73 | EOF 74 | 75 | results ["5",""] 76 | end 77 | 78 | interpret "special syntax assignment with access expression" do 79 | source <<-EOF 80 | e = { f = 5 } 81 | ($e!f ?x) = (inc %x) 82 | put ($e!f 3) 83 | EOF 84 | 85 | result "4" 86 | end 87 | 88 | interpret "body stretching multiple lines" do 89 | source <<-EOF 90 | (f ?x) = ( 91 | y = (inc %x) 92 | z = (inc %y) 93 | put $z 94 | ) 95 | put (f 5) 96 | EOF 97 | 98 | result "7" 99 | end 100 | 101 | interpret "nested body stretching multiple lines" do 102 | source <<-EOF 103 | (f ?x ?y) = ( 104 | z = (?a => put (dec $a) 1) 105 | put (z $x) $y 106 | ) 107 | put (f 1 2) 108 | EOF 109 | 110 | results ["0", "1", "2"] 111 | end 112 | 113 | interpret "body with anon lambda" do 114 | source <<-EOF 115 | put 1 2 3 | each (?a => put 10; put $a) 116 | EOF 117 | 118 | results %w(10 1 10 2 10 3) 119 | end 120 | 121 | interpret "vector patterns" do 122 | source <<-EOF 123 | f = ( 124 | [one ?x] => put [first %x] 125 | [two ?y ?z] => put [second %y %z] 126 | [three] => put [third] 127 | [four ?q to ?w] => put [fourth %q %w] 128 | _ => put default 129 | ) 130 | 131 | f [one 1] 132 | f [two 2 3] 133 | f [three] 134 | f [four 4 to 5] 135 | 136 | # default cases 137 | f [one 1 2] 138 | f [two] 139 | f [three 4] 140 | f [four 5 6] 141 | EOF 142 | 143 | results ["[first 1]", "[second 2 3]", "[third]", "[fourth 4 5]", 144 | "default", "default", "default", "default"] 145 | end 146 | 147 | interpret "one-line vector patterns" do 148 | source <<-EOF 149 | ([x ?y] => put %y) [x 1] 150 | EOF 151 | 152 | results %w(1) 153 | end 154 | 155 | interpret "variable arguments in func assignment" do 156 | source <<-EOF 157 | (f ?x (?rest)) = (put %x; put hello; put %rest) 158 | f 1 2 3 159 | EOF 160 | 161 | results ["1", "hello", "[2 3]"] 162 | end 163 | 164 | interpret "variable arguments in anon assignment" do 165 | source <<-EOF 166 | (?x (?rest) => put %x; put hello; put %rest) 1 2 3 167 | EOF 168 | 169 | results ["1", "hello", "[2 3]"] 170 | end 171 | 172 | interpret 'thing' do 173 | source <<-EOF 174 | range 3 | each (?x => 175 | r = %x 176 | put %r 177 | ) 178 | EOF 179 | 180 | results %w(0 1 2) 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /old-spec/interpret/lexical_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "lexical variables" do 2 | interpret "basic" do 3 | source <<-EOF 4 | (?x => put %x) 1 5 | EOF 6 | 7 | result "1" 8 | end 9 | 10 | interpret "assignment" do 11 | source <<-EOF 12 | x = 2 13 | put %x 14 | EOF 15 | 16 | result "2" 17 | end 18 | 19 | interpret "dynamic assignment" do 20 | source <<-EOF 21 | x = 2 22 | $x = -1 23 | put %x 24 | EOF 25 | 26 | result "-1" 27 | end 28 | 29 | interpret "lexical assignment" do 30 | source <<-EOF 31 | x = 2 32 | %x = -1 33 | put $x 34 | EOF 35 | 36 | result "-1" 37 | end 38 | 39 | interpret "access assignment" do 40 | source <<-EOF 41 | e = { v = 2 } 42 | $e!v = 13 43 | put $e!v 44 | EOF 45 | 46 | result "13" 47 | end 48 | 49 | interpret "mutation" do 50 | source <<-EOF 51 | x = 1 52 | (get-x) = put %x 53 | (set-x ?v) = (%x = $v) 54 | set-x 10 55 | get-x 56 | EOF 57 | 58 | result "10" 59 | end 60 | 61 | interpret "shadowing" do 62 | source <<-EOF 63 | x = 100 64 | f = (?y => add %x %y) 65 | x = 0 66 | f 3 67 | EOF 68 | 69 | result "103" 70 | end 71 | 72 | interpret "missing closure variables" do 73 | source <<-EOF 74 | f = (=> put (=> put %x)) 75 | put should-have-crashed 76 | EOF 77 | 78 | results [] 79 | expect_fail! 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /old-spec/interpret/prelude_spec.rb: -------------------------------------------------------------------------------- 1 | interpret_spec "standard library" do 2 | interpret "range" do 3 | source <<-EOF 4 | range 5 5 | EOF 6 | 7 | results ["0", "1", "2", "3", "4"] 8 | end 9 | 10 | interpret "repeat" do 11 | source <<-EOF 12 | repeat 3 7 13 | EOF 14 | 15 | results ["7", "7", "7"] 16 | end 17 | 18 | interpret "inc" do 19 | source <<-EOF 20 | inc 1 21 | inc 5 22 | EOF 23 | 24 | results ["2", "6"] 25 | end 26 | 27 | interpret "dec" do 28 | source <<-EOF 29 | dec 1 30 | dec 5 31 | EOF 32 | 33 | results ["0", "4"] 34 | end 35 | 36 | interpret "all" do 37 | source <<-EOF 38 | range 2 | all (?x => lt 10 %x) 39 | EOF 40 | 41 | results [] 42 | end 43 | 44 | interpret "combining functions" do 45 | source <<-EOF 46 | put 0 100 | each (?v => iter (?n => inc %n) %v | take 3) 47 | EOF 48 | 49 | results ["0", "1", "2", "100", "101", "102"] 50 | end 51 | 52 | interpret "flaky test on composition" do 53 | source <<-EOF 54 | (repeat-func ?fn) = (produce (=> put (exec %fn))) 55 | (get-first-two-elems-in-sequence ?fn ?start) = (iter %fn %start | take 2) 56 | repeat-func [range 5] | each (?n => get-first-two-elems-in-sequence [inc] %n) | 57 | filter [even] | take 1 58 | EOF 59 | 60 | results ["0"] 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /old-spec/lexer_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Lexer do 2 | abstract(:input) 3 | let(:lex) { Magritte::Lexer.new("test",input) } 4 | let(:tokens) { lex.to_a } 5 | 6 | # Define helper functions for setting up tests 7 | # Example: Magritte::AST::Variable["foo"] = _variable("foo") 8 | define_method("_token") do |type, value = nil, range = nil| 9 | Magritte::Lexer::Token.new(type, value, range) 10 | end 11 | 12 | describe "some delimiters" do 13 | let(:input) { 14 | """ 15 | ( [ $hoge 16 | """ 17 | } 18 | 19 | it "parses basic delimiters" do 20 | assert { tokens == [_token(:lparen), _token(:lbrack), _token(:var,"hoge"), _token(:nl), _token(:eof)] } 21 | end 22 | end 23 | 24 | describe "some basic keywords" do 25 | let(:input) { 26 | """ 27 | [({} ) ] = %a 28 | """ 29 | } 30 | 31 | it "parses basic keywords" do 32 | assert { tokens == [_token(:lbrack), _token(:lparen), _token(:lbrace), _token(:rbrace), _token(:rparen), _token(:rbrack), _token(:equal), _token(:lex_var, "a"), _token(:nl), _token(:eof)] } 33 | end 34 | end 35 | 36 | describe "function header" do 37 | let(:input) { 38 | """ 39 | (func ?n) 40 | """ 41 | } 42 | 43 | it "parses basic function header" do 44 | assert { tokens == [_token(:lparen), _token(:bare, "func"), _token(:bind, "n"), _token(:rparen), _token(:nl), _token(:eof)] } 45 | end 46 | end 47 | 48 | describe "error recovery tokens" do 49 | let(:input) { 50 | """ 51 | || && !! %%%%! 52 | """ 53 | } 54 | 55 | it "parses tokens correctly" do 56 | assert { tokens == [_token(:d_bar), _token(:d_amp), _token(:d_bang), _token(:d_per), _token(:d_per_bang), _token(:nl), _token(:eof)] } 57 | end 58 | end 59 | 60 | describe "oprators" do 61 | let(:input) { 62 | """ 63 | | & == => 64 | """ 65 | } 66 | 67 | it "parses operators" do 68 | assert { tokens == [_token(:pipe), _token(:amp), _token(:equal), _token(:equal), _token(:arrow), _token(:nl), _token(:eof)] } 69 | end 70 | end 71 | 72 | describe "numbers" do 73 | let(:input) { 74 | """ 75 | 2 6.28 0.00001 1. -5.4 76 | """ 77 | } 78 | 79 | it "parses numbers correctly" do 80 | assert { tokens == [_token(:num, "2"), _token(:num, "6.28"), _token(:num, "0.00001"), _token(:num, "1."), _token(:num,"-5.4"), _token(:nl), _token(:eof)] } 81 | end 82 | end 83 | 84 | describe "strings" do 85 | let(:input) { 86 | """ 87 | \"asksnz-zwjdfqw345 r8 ewn ih2wu\\\" wihf002+4-r9+***.m.-< \\\"\" 88 | """ 89 | } 90 | 91 | it "parses strings correctly" do 92 | assert { tokens == [_token(:string, "asksnz-zwjdfqw345 r8 ewn ih2wu\\\" wihf002+4-r9+***.m.-< \\\""), _token(:nl), _token(:eof)] } 93 | end 94 | end 95 | 96 | describe "check that between method raises error" do 97 | let(:input) { 98 | """ 99 | a 100 | """ 101 | } 102 | 103 | it "must raise an error if source names differ" do 104 | lex2 = Magritte::Lexer.new("test2", input) 105 | err = assert_raises { Magritte::Lexer::Range.between(lex2.to_a.first, tokens.first) } 106 | assert { err.message == "Can't compute Range.between, mismatching source names: test2 != test" } 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /old-spec/matcher_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Matcher do 2 | include Magritte::Matcher::DSL 3 | abstract(:input) 4 | let(:lex) { Magritte::Lexer.new("test",input) } 5 | let(:tree) { Magritte::Skeleton::Parser.parse(lex).elems.first } 6 | let(:matcher) { raise "Abstract" } 7 | let(:match_vars) { matcher.match_vars(tree) } 8 | 9 | describe "a single token" do 10 | let(:input) { 11 | """ 12 | $x 13 | """ 14 | } 15 | 16 | describe "underscore" do 17 | let(:matcher) { singleton(~_) } 18 | 19 | it "works" do 20 | assert { match_vars.size == 1 } 21 | assert { match_vars.first.repr == ".var/x" } 22 | end 23 | end 24 | 25 | describe "token matcher" do 26 | let(:matcher) { singleton(~token(:var)) } 27 | 28 | it "works" do 29 | assert { match_vars.size == 1 } 30 | assert { match_vars.first.repr == ".var/x" } 31 | end 32 | end 33 | 34 | describe "failed token matcher" do 35 | let(:matcher) { singleton(~token(:pipe)) } 36 | 37 | it "fails" do 38 | assert { match_vars.nil? } 39 | end 40 | end 41 | end 42 | 43 | describe "nesting" do 44 | let(:input) { 45 | """ 46 | (func) 47 | """ 48 | } 49 | 50 | describe "underscore" do 51 | let(:matcher) { singleton(~_) } 52 | 53 | it "works" do 54 | assert { match_vars.size == 1 } 55 | assert { match_vars.first.repr == "[lparen|.bare/func|rparen]" } 56 | end 57 | end 58 | 59 | describe "nested" do 60 | let(:matcher) { singleton(~nested(:lparen,_)) } 61 | 62 | it "works" do 63 | assert { match_vars.size == 1 } 64 | assert { match_vars.first.repr == "[lparen|.bare/func|rparen]" } 65 | end 66 | end 67 | 68 | describe "specific nested" do 69 | let(:matcher) { singleton(~nested(:lparen,singleton(token(:bare)))) } 70 | 71 | it "works" do 72 | assert { match_vars.size == 1 } 73 | assert { match_vars.first.repr == "[lparen|.bare/func|rparen]" } 74 | end 75 | end 76 | end 77 | 78 | describe "start/end" do 79 | let(:input) { 80 | """ 81 | g %a 82 | """ 83 | } 84 | 85 | describe "test start" do 86 | let(:matcher) { ~starts(token(:bare),~singleton(token(:lex_var))) } 87 | 88 | it "works" do 89 | assert { match_vars.size == 2 } 90 | assert { match_vars[0].repr == "(.lex_var/a)" } 91 | assert { match_vars[1].repr == "(.bare/g .lex_var/a)" } 92 | end 93 | end 94 | 95 | describe "test end" do 96 | let(:matcher) { ~ends(token(:lex_var),singleton(token(:bare))) } 97 | 98 | it "works" do 99 | assert { match_vars.size == 1 } 100 | assert { match_vars.first.repr == "(.bare/g .lex_var/a)" } 101 | end 102 | end 103 | end 104 | 105 | describe "splits" do 106 | let(:input) { 107 | """ 108 | p1 | p2 | p3 109 | """ 110 | } 111 | 112 | describe "test lsplit" do 113 | let(:matcher) { lsplit(~singleton(token(:bare)),token(:pipe),~_) } 114 | 115 | it "works" do 116 | assert { match_vars.size == 2 } 117 | assert { match_vars[0].repr == "(.bare/p1)" } 118 | assert { match_vars[1].repr == "(.bare/p2 .pipe .bare/p3)" } 119 | end 120 | end 121 | 122 | describe "test rsplit" do 123 | let(:matcher) { rsplit(~_,token(:pipe),~_) } 124 | 125 | it "works" do 126 | assert { match_vars.size == 2 } 127 | assert { match_vars[0].repr == "(.bare/p1 .pipe .bare/p2)" } 128 | assert { match_vars[1].repr == "(.bare/p3)" } 129 | end 130 | end 131 | end 132 | 133 | describe "lambda" do 134 | let(:input) { 135 | """ 136 | (f ?x) = add $x 1 137 | """ 138 | } 139 | 140 | describe "test lsplit" do 141 | let(:matcher) { lsplit(~_, token(:equal), ~_) } 142 | 143 | it "works" do 144 | assert { match_vars.size == 2 } 145 | assert { match_vars[0].repr == "([lparen|.bare/f .bind/x|rparen])" } 146 | assert { match_vars[1].repr == "(.bare/add .var/x .num/1)" } 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /old-spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Parser do 2 | abstract(:input) 3 | 4 | let(:lex) { Magritte::Lexer.new("test",input) } 5 | let(:skel) { Magritte::Skeleton::Parser.parse(lex) } 6 | let(:ast) { Magritte::Parser.parse_root(skel) } 7 | 8 | describe "a vector" do 9 | let(:input) { 10 | """ 11 | put [a b c] 12 | """ 13 | } 14 | 15 | it "parses correctly" do 16 | assert { ast.elems.size == 1 } 17 | assert { ast.elems.first.is_a?(Magritte::AST::Command) } 18 | assert { ast.elems.first.vec.size == 2 } 19 | assert { ast.elems.first.vec.inspect == "[#, #, #, #]]>]" } 20 | end 21 | end 22 | 23 | describe "access" do 24 | let(:input) { 25 | """ 26 | put $foo!bar 27 | """ 28 | } 29 | 30 | it "parses correctly" do 31 | assert { ast.elems.size == 1 } 32 | assert { ast.elems.first.vec[1].is_a?(Magritte::AST::Access) } 33 | end 34 | end 35 | 36 | describe "pipe" do 37 | let(:input) { 38 | """ 39 | f $a | put 40 | """ 41 | } 42 | 43 | it "parses correctly" do 44 | assert { ast.elems.size == 1 } 45 | assert { ast.elems.first.is_a?(Magritte::AST::Pipe) } 46 | assert { ast.elems.first.producer.is_a?(Magritte::AST::Command) } 47 | assert { ast.elems.first.consumer.is_a?(Magritte::AST::Command) } 48 | end 49 | end 50 | 51 | describe "spawn" do 52 | let(:input) { 53 | """ 54 | & command arg1 55 | """ 56 | } 57 | 58 | it "parses correctly" do 59 | assert { ast.elems.size == 1 } 60 | assert { ast.elems.first.is_a?(Magritte::AST::Spawn) } 61 | assert { ast.elems.first.expr.is_a?(Magritte::AST::Command) } 62 | end 63 | end 64 | 65 | describe "rescue operators" do 66 | let(:input) { 67 | """ 68 | a && b || c !! d 69 | """ 70 | } 71 | 72 | it do 73 | assert { ast.elems.size == 1 } 74 | assert { ast.elems.first.is_a?(Magritte::AST::And) } 75 | assert { ast.elems.first.lhs.is_a?(Magritte::AST::Command) } 76 | assert { ast.elems.first.lhs.vec.size == 1 } 77 | assert { ast.elems.first.lhs.vec.first.value == "a" } 78 | assert { ast.elems.first.rhs.is_a?(Magritte::AST::Else) } 79 | assert { ast.elems.first.rhs.lhs.is_a?(Magritte::AST::Or) } 80 | end 81 | end 82 | 83 | describe "switch statement" do 84 | let(:input) { 85 | """ 86 | cond && c !! cond2 && c2 !! cond3 && c3 !! c4 87 | """ 88 | } 89 | 90 | it do 91 | assert { ast.elems.size == 1 } 92 | assert { ast.elems.first.is_a?(Magritte::AST::Else) } 93 | assert { ast.elems.first.rhs.is_a?(Magritte::AST::Else) } 94 | assert { ast.elems.first.rhs.rhs.is_a?(Magritte::AST::Else) } 95 | assert { ast.elems.first.lhs.is_a?(Magritte::AST::And) } 96 | assert { ast.elems.first.rhs.lhs.is_a?(Magritte::AST::And) } 97 | assert { ast.elems.first.rhs.rhs.lhs.is_a?(Magritte::AST::And) } 98 | assert { ast.elems.first.rhs.rhs.rhs.vec.first.value == "c4" } 99 | end 100 | end 101 | 102 | describe "compensation" do 103 | let(:input) { 104 | """ 105 | c a1 %% c2 a2 106 | """ 107 | } 108 | 109 | it do 110 | assert { ast.elems.size == 1 } 111 | assert { ast.elems.first.is_a?(Magritte::AST::Compensation) } 112 | assert { ast.elems.first.expr.is_a?(Magritte::AST::Command) } 113 | assert { ast.elems.first.compensation.is_a?(Magritte::AST::Command) } 114 | assert { ast.elems.first.expr.vec.size == 2 } 115 | assert { ast.elems.first.expr.vec[0].value == "c" } 116 | assert { ast.elems.first.expr.vec[1].value == "a1" } 117 | assert { ast.elems.first.compensation.vec.size == 2 } 118 | assert { ast.elems.first.compensation.vec[0].value == "c2" } 119 | assert { ast.elems.first.compensation.vec[1].value == "a2" } 120 | assert { ast.elems.first.unconditional == :conditional } 121 | end 122 | end 123 | 124 | describe "compensation with checkpoints" do 125 | let(:input) { 126 | """ 127 | c a1 %%! c2 a2 128 | """ 129 | } 130 | 131 | it do 132 | assert { ast.elems.size == 1 } 133 | assert { ast.elems.first.is_a?(Magritte::AST::Compensation) } 134 | assert { ast.elems.first.expr.is_a?(Magritte::AST::Command) } 135 | assert { ast.elems.first.compensation.is_a?(Magritte::AST::Command) } 136 | assert { ast.elems.first.expr.vec.size == 2 } 137 | assert { ast.elems.first.expr.vec[0].value == "c" } 138 | assert { ast.elems.first.expr.vec[1].value == "a1" } 139 | assert { ast.elems.first.compensation.vec.size == 2 } 140 | assert { ast.elems.first.compensation.vec[0].value == "c2" } 141 | assert { ast.elems.first.compensation.vec[1].value == "a2" } 142 | assert { ast.elems.first.unconditional == :unconditional } 143 | end 144 | end 145 | 146 | describe "only one compensation per line" do 147 | let(:input) { 148 | """ 149 | c a1 %% c2 a2 %% c3 150 | """ 151 | } 152 | 153 | it do 154 | err = assert_raises { ast } 155 | assert { err.message =~ /\Aunrecognized syntax at test@/ } 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /old-spec/skeleton_spec.rb: -------------------------------------------------------------------------------- 1 | describe Magritte::Skeleton do 2 | abstract(:input) 3 | let(:lex) { Magritte::Lexer.new("test",input) } 4 | let(:tree) { Magritte::Skeleton::Parser.parse(lex) } 5 | 6 | describe "well balanced parenthesis" do 7 | let(:input) { 8 | """ 9 | (a ?n) 10 | """ 11 | } 12 | 13 | it "is accepted by skeleton tree" do 14 | assert { tree.repr == "(([lparen|.bare/a .bind/n|rparen]))"} 15 | end 16 | end 17 | 18 | describe "expression with nested scope" do 19 | let(:input) { 20 | """ 21 | (a ?x) = (a (b c d) e) 22 | """ 23 | } 24 | 25 | it "is parsed correctly" do 26 | assert { tree.repr == "(([lparen|.bare/a .bind/x|rparen] .equal [lparen|.bare/a [lparen|.bare/b .bare/c .bare/d|rparen] .bare/e|rparen]))" } 27 | end 28 | end 29 | 30 | describe "multi-line program" do 31 | let(:input) { 32 | """ 33 | x = 5 34 | a = add $x 2 35 | """ 36 | } 37 | 38 | it "is parsed as two items" do 39 | assert { tree.repr == "((.bare/x .equal .num/5) (.bare/a .equal .bare/add .var/x .num/2))" } 40 | end 41 | end 42 | 43 | describe "nested scope startin and ending on different lines" do 44 | let(:input) { 45 | """ 46 | x = { 47 | a = \"book\" 48 | b = (f 5 %s $d) 49 | y = { 50 | } 51 | } 52 | """ 53 | } 54 | 55 | it "is parsed correctly" do 56 | assert { tree.repr == "((.bare/x .equal [lbrace|(.bare/a .equal .string/book) (.bare/b .equal [lparen|.bare/f .num/5 .lex_var/s .var/d|rparen]) (.bare/y .equal [lbrace||rbrace])|rbrace]))" } 57 | end 58 | end 59 | 60 | describe "scope using square brackets" do 61 | let(:input) { 62 | """ 63 | s [ 64 | (=> c > %ch) 65 | (=> d < %ch) 66 | ] 67 | """ 68 | } 69 | 70 | it "parses correctly" do 71 | assert { tree.repr == "((.bare/s [lbrack|[lparen|.arrow .bare/c .gt .lex_var/ch|rparen] [lparen|.arrow .bare/d .lt .lex_var/ch|rparen]|rbrack]))" } 72 | end 73 | end 74 | 75 | describe "more tests" do 76 | let(:input) { 77 | """ 78 | $foo!bar 79 | """ 80 | } 81 | 82 | it "parses correctly" do 83 | assert { tree.repr == "((.var/foo .bang .bare/bar))" } 84 | end 85 | end 86 | 87 | describe "nesting error" do 88 | let(:input) { 89 | """ 90 | [b 91 | """ 92 | } 93 | 94 | it "throws an error" do 95 | err = assert_raises { tree } 96 | assert { err.message == "Unmatched nesting at test@2:7~3:1" } 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /old-spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | Bundler.require 6 | require 'magritte' 7 | require 'minitest/spec' 8 | require 'minitest/autorun' 9 | 10 | Dir[File.expand_path('support/**/*.rb', File.dirname(__FILE__))].each {|f| 11 | require f 12 | } 13 | -------------------------------------------------------------------------------- /old-spec/support/abstract.rb: -------------------------------------------------------------------------------- 1 | module HasAbstract 2 | def abstract(*a) 3 | let(*a) { raise "abstract: #{a.map(&:inspect).join(', ')}" } 4 | end 5 | end 6 | 7 | Minitest::Spec.send(:extend, HasAbstract) 8 | -------------------------------------------------------------------------------- /old-spec/support/focus.rb: -------------------------------------------------------------------------------- 1 | module SpecFocus 2 | FILTERED_NAMES = [] 3 | class Focus 4 | def initialize(context) 5 | @context = context 6 | end 7 | 8 | def method_missing(name, *a, &b) 9 | # unnecessary because we're forwarding anyways, and we 10 | # *want* to be able to call private methods 11 | ## return super unless @context.methods.include?(name) 12 | 13 | res = @context.send(name, *a, &b) 14 | 15 | register_focus(res.to_s) 16 | end 17 | 18 | private 19 | def register_focus(name) 20 | FILTERED_NAMES << name 21 | ARGV.reject! { |arg| arg.start_with?('-n') } 22 | 23 | filter = "/\\A#{Regexp.union(FILTERED_NAMES).source}/" 24 | ARGV << "-n=#{filter}" 25 | end 26 | end 27 | 28 | module DSL 29 | def f 30 | Focus.new(self) 31 | end 32 | end 33 | end 34 | 35 | Minitest::Spec.send(:extend, SpecFocus::DSL) 36 | include SpecFocus::DSL 37 | -------------------------------------------------------------------------------- /old-spec/support/infinity.rb: -------------------------------------------------------------------------------- 1 | Infinity = 1.0/0 2 | -------------------------------------------------------------------------------- /old-spec/support/interpret_helpers.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | require 'fileutils' 3 | require 'open3' 4 | require 'timeout' 5 | 6 | module InterpretHelpers 7 | TMP_DIR = Pathname.new('./tmp/spec-build') 8 | TMP_DIR.mkpath 9 | VM_PATH = ENV['MAGRITTE_VM'] || './build/magvm' 10 | 11 | def self.included(base) 12 | base.send(:extend, ClassMethods) 13 | 14 | base.class_eval do 15 | abstract(:input) 16 | 17 | let(:lex) { Magritte::Lexer.new(input_name, input) } 18 | let(:skel) { Magritte::Skeleton::Parser.parse(lex) } 19 | let(:ast) { Magritte::Parser.parse(skel) } 20 | let(:compiler) { Magritte::Compiler.new(ast).compile } 21 | 22 | let(:do_run) do 23 | File.open("#{tmp_file}x", 'w') { |f| compiler.render_decomp(f) } 24 | tmp_file.open('w') { |f| compiler.render(f) } 25 | 26 | env = { 'MAGRITTE_DEBUG_TO' => '2' } 27 | script = "MAGRITTE_DEBUG_TO=2 #{VM_PATH} #{tmp_file} > #{tmp_file}.out 2>#{tmp_file}.err" 28 | 29 | puts 'SCRIPT' 30 | puts script 31 | 32 | pid = nil 33 | begin 34 | Timeout.timeout(2) do 35 | pid = Process.spawn(script) 36 | @status = Process.wait(pid) 37 | end 38 | rescue Timeout::Error 39 | pid && Process.kill('TERM', pid) 40 | end 41 | end 42 | 43 | let(:debug) { do_run; File.read("#{tmp_file}.err") } 44 | let(:result) { do_run; File.read("#{tmp_file}.out").strip } 45 | let(:status) { do_run; @status } 46 | 47 | let(:results) { result.split("\n") } 48 | end 49 | end 50 | 51 | module ClassMethods 52 | def interpret(description, &b) 53 | dsl = DSL.new 54 | spec = dsl.evaluate(&b) 55 | describe description do 56 | let(:input) { "load ./mag/prelude.mag\n\n#{spec.source}" } 57 | let(:input_name) { "#{spec.source_name}: #{self.class.to_s}" } 58 | let(:tmp_file) { TMP_DIR.join(spec.source_name.gsub(/[^\w]/, '.')) } 59 | 60 | it do 61 | # puts debug 62 | 63 | source_name = spec.source_name 64 | if spec.expect_success 65 | assert { source_name; status == 0 } 66 | else 67 | assert { source_name; status && status != 0 } 68 | end 69 | 70 | spec.result_expectations.each { |b| instance_eval(&b) } 71 | end 72 | end 73 | end 74 | end 75 | 76 | class DSL 77 | def initialize 78 | @source = 'crash "source undefined"' 79 | @source_loc = nil 80 | @status_expectations = [] 81 | @result_expectations = [] 82 | @expect_succeed = true 83 | end 84 | 85 | def source(source) 86 | @source_loc = caller[0] 87 | @source = source 88 | end 89 | 90 | def result(*output) 91 | results(output) 92 | end 93 | 94 | def expect_fail! 95 | @expect_succeed = false 96 | end 97 | 98 | def results(outputs) 99 | source_loc = @source_loc 100 | @result_expectations << proc do 101 | assert { source_loc; results == outputs } 102 | end 103 | end 104 | 105 | def results_size(len) 106 | source_loc = @source_loc 107 | @result_expectations << proc do 108 | assert { source_loc; results.size == len } 109 | end 110 | end 111 | 112 | def debug 113 | @result_expectations << proc do 114 | binding.pry 115 | end 116 | end 117 | 118 | def evaluate(&b) 119 | instance_eval(&b) 120 | OpenStruct.new( 121 | source: @source, 122 | result_expectations: @result_expectations, 123 | expect_success: @expect_success, 124 | source_name: self.source_name, 125 | ) 126 | end 127 | 128 | def source_name 129 | return '(anon)' if @source_loc.nil? 130 | @source_loc =~ /\A(.*?:\d+):/ 131 | filename, line = $1.split(':') 132 | "test@#{filename}:#{line}" 133 | end 134 | 135 | end 136 | end 137 | 138 | def interpret_spec(description, &b) 139 | describe(description) do 140 | include InterpretHelpers 141 | class_eval(&b) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /old-spec/support/rescuing.rb: -------------------------------------------------------------------------------- 1 | module Rescuing 2 | def rescuing(type=Exception) 3 | yield 4 | nil 5 | rescue type => e 6 | e 7 | end 8 | end 9 | 10 | Object.send(:include, Rescuing) 11 | -------------------------------------------------------------------------------- /test/framework.mag: -------------------------------------------------------------------------------- 1 | (skip ?reason ?name ?fn) = puts "- skip:" $name " (" $reason ")" 2 | (test ?name ?fn) = ( 3 | ( 4 | %name => run-test %name %fn 5 | => run-test %name %fn 6 | _ => skip not-focused %name %fn 7 | ) (getenv MAGRITTE_FOCUS) 8 | ) 9 | 10 | (run-test ?name ?fn) = ( 11 | vm-debug on 12 | puts "- test:" $name 13 | vm-debug off 14 | %fn && put "-- pass" !! put "-- fail" 15 | ) 16 | -------------------------------------------------------------------------------- /test/mag/basic.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test nested-put (=> 4 | eq (put (put 1)) 1 5 | ) 6 | 7 | test unsplat (=> 8 | (f ...?a) = put $a 9 | eq (f 1 2 3) [1 2 3] 10 | ) 11 | 12 | test splat (=> 13 | args = [1 1] 14 | eq ...$args 15 | ) 16 | -------------------------------------------------------------------------------- /test/mag/channels.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test channels (=> 4 | c = (make-channel) 5 | 6 | out = [( 7 | ( 8 | put 1 %% (put comp > %c) 9 | put 2 3 4 5 6 10 | ) | take 2 11 | 12 | get < $c 13 | )] 14 | 15 | eq $out [1 2 comp] 16 | ) 17 | -------------------------------------------------------------------------------- /test/mag/concurrent.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test wait-for-close (=> 4 | out = [(& put 1)] 5 | eq [1] $out 6 | ) 7 | 8 | test thread-into-collector (=> 9 | out = [0 (put 1 2 3 4 5 6 7 8 9 10 | (& drain; & drain))] 10 | eq 11 (len $out) 11 | ) 12 | 13 | test slide-example (=> 14 | count-forever | (& drain; & drain) | take 10 15 | ) 16 | 17 | test no-starvation (=> 18 | count-forever | (& label a; & label b) | take 20 19 | ) 20 | -------------------------------------------------------------------------------- /test/mag/lambda.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test funcdef-group-env (=> 4 | e = {} 5 | ($e!bar a) = put b 6 | ($e!bar b) = put a 7 | 8 | eq [($e!bar a) ($e!bar b)] [b a] 9 | ) 10 | 11 | test funcdef-group (=> 12 | (foo a) = put b 13 | (foo b) = put a 14 | 15 | eq [(foo a) (foo b)] [b a] 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /test/mag/oop.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test modules (=> 4 | my-module = { 5 | a = 1 6 | b = 2 7 | (thing ?c) = add %a %b %c 8 | } 9 | 10 | eq ($my-module!thing 6) 9 11 | ) 12 | 13 | test dynamic-assign (=> 14 | e = {} 15 | key = foo 16 | $e!$key = bar 17 | 18 | eq bar $e!foo 19 | ) 20 | 21 | test dynamic-get (=> 22 | e = {} 23 | key = foo 24 | $e!foo = bar 25 | 26 | eq bar $e!$key 27 | ) 28 | -------------------------------------------------------------------------------- /test/mag/process.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | skip make-crash-deregister crash (=> 4 | out = (& (put 3; crash)) 5 | 6 | eq $out "" 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/mag/recursion.mag: -------------------------------------------------------------------------------- 1 | require test/framework.mag 2 | 3 | test iter (=> 4 | iter [add 1] 0 | each mul 3 | take 1 5 | 6 | # TODO: fix wait-for-close so we can actually test the output lol 7 | eq 1 1 8 | ) 9 | 10 | test alternating-iter (=> 11 | iter ( 12 | [a ?x] => put [b (add 1 $x)] 13 | [b ?y] => put [a (add 2 $y)] 14 | ) [a 1] 15 | | take 10 16 | 17 | eq 1 1 18 | ) 19 | 20 | test pipe-cases (=> 21 | ( 22 | put [a 1] 23 | put [b 2] 24 | put [a 3] 25 | put [b 4] 26 | put [c done] 27 | ) | each ( 28 | [a ?x] => put [a (mul 10 $x)] 29 | [b ?y] => put [b (mul 100 $y)] 30 | [c ?z] => put cool 31 | ) 32 | 33 | # `each` is falsey, since it can be interrupted 34 | eq 1 1 35 | ) 36 | 37 | test each-continues (=> 38 | (sum) = ( 39 | out = 0 40 | each (?x => %out = (add $x %out)) 41 | put $out 42 | ) 43 | 44 | eq (put 1 2 3 | sum) 6 45 | ) 46 | 47 | test filter (=> 48 | out = [( 49 | (put [a 1]; put [b 2]; put [a 3]; put [b 4]) 50 | | filter ([a _] => true; _ => false) 51 | )] 52 | 53 | eq $out [[a 1] [a 3]] 54 | ) 55 | -------------------------------------------------------------------------------- /test/test.mag: -------------------------------------------------------------------------------- 1 | load test/mag/basic.mag 2 | load test/mag/channels.mag 3 | load test/mag/concurrent.mag 4 | load test/mag/lambda.mag 5 | load test/mag/oop.mag 6 | load test/mag/process.mag 7 | load test/mag/recursion.mag 8 | -------------------------------------------------------------------------------- /vim/syntax/mag.vim: -------------------------------------------------------------------------------- 1 | " Vim syntax file 2 | " Language: Magritte 3 | " Maintainer: Jeanine Adkisson 4 | 5 | let ident = "[a-zA-Z-][\/a-zA-Z0-9_-]*" 6 | 7 | "" dash is anywhere in an ident 8 | setlocal iskeyword+=- 9 | 10 | syntax sync fromstart 11 | 12 | " syntax match magPunctuation /\%(:\|,\|\;\|!\|<\|\*\|>\|=\|(\|)\|\[\|\]\||\|{\|}\|\~\)/ 13 | syntax match magPunctuation /\%(|\|=>\|;\|(\|)\|\[\|\]\|<\|>\|{\|}\|&\|!\)/ 14 | 15 | 16 | syn match magComment /#[^\n]*\n/ 17 | exe "syn match magAnnot /++\\?" . ident . "/" 18 | exe "syn match magName /" . ident . "/" 19 | " exe "syn match magDotted /[.][\\/]\\?" . ident . "/" 20 | exe "syn match magCheck /[\\%]" . ident . "/" 21 | exe "syn match magPath /[\\:]" . ident . "/" 22 | exe "syn match magLookup /[!]" . ident . "/" 23 | exe "syn match magKeyword /[@][@!]\\?" . ident . "/" 24 | exe "syn match magDollar /[\\$]/" 25 | exe "syn match magBinder /[\\?]" . ident . "/" 26 | exe "syn match magDynamic /[\\$]" . ident . "/" 27 | exe "syn match magDynamic /[\\$][0-9]\\+/" 28 | exe "syn match magMacro /\\(\\\\\\\\\\?" . ident . "\\)/" 29 | exe "syn match magFlag /-" . ident . "/" 30 | exe "syn match magInfix /`" . ident . "/" 31 | " syn match magUppercase /[A-Z][a-zA-z0-9]*/ 32 | 33 | syn match magNumber /\d\+\(\.\d\+\)\?\>/ 34 | 35 | " syn match magBareString /'[^{][^ \n)\];]*/ 36 | " syn region magParseMacro start=/\\\w\+{/ end="" contains=magStringContents 37 | " syn region magString start="'{" end="" contains=magStringContents 38 | " syn region magStringContents start="{" end="}" contains=magStringContents contained 39 | 40 | syn region magDQString start='"' end='"' contains=magUnicode,magEscape 41 | syn match magUnicode /\\u[0-9a-f][0-9a-f][0-9a-f][0-9a-f]/ contained 42 | syn match magEscape /\\[trn0e\\"]/ contained 43 | 44 | hi! def link magName Name 45 | hi! def link magUppercase Type 46 | hi! def link magDotted Type 47 | hi! def link magPunctuation Punctuation 48 | hi! def link magCheck Type 49 | hi! def link magKeyword Keyword 50 | hi! def link magMacro Punctuation 51 | hi! def link magFlag Special 52 | " hi! def link magBareString String 53 | " hi! def link magString String 54 | " hi! def link magParseMacro Punctuation 55 | hi! def link magDQString String 56 | hi! def link magPath String 57 | hi! def link magLookup Function 58 | hi! def link magUnicode SpecialChar 59 | hi! def link magEscape SpecialChar 60 | hi! def link magStringContents String 61 | hi! def link magAnnot Function 62 | hi! def link magInfix Function 63 | hi! def link magLet Punctuation 64 | hi! def link magDynamic Identifier 65 | hi! def link magBinder Special 66 | hi! def link magDollar Identifier 67 | hi! def link magComment Comment 68 | hi! def link magNumber Number 69 | --------------------------------------------------------------------------------