├── CREDITS ├── README ├── TODO ├── examples ├── learn-the-hell-out-of-regular-expressions │ ├── README.py │ ├── TOC.py │ ├── trivial_match.py │ ├── trivial_match_soln.py │ ├── whats_a_regex.py │ └── whats_a_regex_soln.py ├── perfectmedians.lhs ├── sample.el ├── sample.js ├── sample.lhs ├── sample.py ├── sample.sh └── sokoban.py ├── ghci-halp.sh ├── ghcihalp.py ├── halp.el ├── pyhalp.py ├── sh-halp.sh ├── tests ├── expected.sample.py └── test-pyhalp.sh ├── v8halp.cc ├── v8halp.mk └── v8halp.py /CREDITS: -------------------------------------------------------------------------------- 1 | The first version was written at a party in 2006 by Darius Bacon, 2 | Brandon Moore, and Evan Murphy. 3 | 4 | It was developed into something vaguely practical in 2008 by Darius. 5 | 6 | Thanks to Nada Amin for getting it to work on Windows, Python 2.4, and Emacs 21. 7 | 8 | Thanks to Richard Uhtenwoldt for making me add Emacs Lisp support and 9 | pair-programming it with me. 10 | 11 | For feedback, thanks to: 12 | 13 | Shae Erisson 14 | Tim Chevalier 15 | Kragen Sitaker 16 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | HALP 2 | 3 | With Halp, one keystroke executes all specially-marked lines from a 4 | buffer and inserts the results inline. It can do this for source code 5 | in Python, Haskell (literate or illiterate), or sh. This helps you 6 | interactively test your programs as you write them -- like a 7 | read-eval-print loop, but different. 8 | 9 | To try it out, first install halp.el as described below. Then visit a 10 | suitable file, (like sample.py, sample.lhs, or sample.sh in 11 | examples/), and hit M-i. These sample files will explain what you can 12 | do and how it works. (Actually only sample.py explains much. But for 13 | the other languages, currently, there's little to explain.) 14 | 15 | 16 | INSTALLING 17 | 18 | (NOTE: While the Python Halp is in active use as of 2023, the others 19 | should be considered old demos and no more.) 20 | 21 | Add this line to your .emacs: 22 | 23 | (load-file "/path/to/this/directory/halp.el") 24 | 25 | or just do M-x load-file halp.el. (Where /path/to/this/directory/ is 26 | where this README file and the rest of Halp resides.) 27 | 28 | It will bind M-i in the modes that Halp supports. (Edit halp.el if you 29 | want to change this.) 30 | 31 | You'll need python-mode, or haskell-mode, etc., installed already 32 | (whichever of these you intend to use with Halp). You'll also need 33 | Emacs >= 21. Python 2 and 3 are both supported. 34 | 35 | To build and install the JavaScript support: 36 | 37 | 1. Build V8 from http://code.google.com/p/v8/ 38 | 2. Copy or symlink include/ and libv8.a from the directory you built 39 | V8 in (probably named v8-read-only) 40 | 3. Run "make -f v8halp.mk" 41 | 4. Put the resulting v8halp binary in your $PATH. 42 | 43 | 44 | AUTHORS 45 | 46 | Darius Bacon 47 | Brandon Moore 48 | Evan Murphy 49 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | *********** general: 2 | 3 | make an emacs package now that emacs has a package system 4 | 5 | auto-timeout the helper-subprocess runs 6 | 7 | fill out test suite 8 | 9 | other languages: 10 | lisp 11 | tush 12 | 13 | doctest support -- mark an output as 'correct', and tell me when 14 | there's an error 15 | 16 | test isolation 17 | 18 | allow multiline examples (and outputs?) 19 | 20 | syntax coloring 21 | 22 | support "literate python"? noweb? 23 | 24 | insert results asynchronously; don't hang up emacs 25 | 26 | persistent process to avoid startup time, if possible 27 | 28 | clean up code: 29 | halp.el has some ugly bits still lying around 30 | group stuff into an examples directory 31 | 32 | wouldn't it be better to centralize the diffing in halp.el instead of 33 | in each helper? *tries to remember why I didn't* 34 | 35 | 36 | *********** halp.el: 37 | 38 | (defconst selectric-files-path (file-name-directory load-file-name)) 39 | 40 | when the output changes, take the cursor to the first change, 41 | with the mark set so you can go back 42 | 43 | add doc comments to interactive elisp functions 44 | 45 | continuous halp mode: auto-rerun halp after every change to the buffer; 46 | but don't show changed output after these auto-reruns (too disruptive); 47 | instead just change the prefixes on outputs that change (to '#X ', say). 48 | 49 | move cursor to position reported by compiler error message 50 | 51 | 52 | *********** pyhalp: 53 | 54 | check against test results (#= prefix or something) 55 | 56 | make up a key-command to convert the textually next output into 57 | a test expectation 58 | 59 | don't delete old outputs if there's an error in the initial module 60 | loading. but do mark them somehow as no longer the current output. 61 | 62 | halp.print(foo) => prints the result inline at the point in the 63 | source it occurs (or as close as we can figure out), instead of 64 | being collected with the general stdout output 65 | 66 | may be useful: 67 | inspect.currentframe()/getouterframes()/getinnerframes()/getframeinfo() 68 | 69 | We're still getting 'File "", line 14' on syntax errors. 70 | It looks like that message comes out of the SyntaxError exception 71 | rather than the traceback. I tried setting value.filename = halp_filename 72 | in get_lineno, but that did nothing. Come back to this. 73 | 74 | more concise/useful Python error messages 75 | 76 | single-stepping of some sort 77 | 78 | make sure python isn't compiling pyhalp.py every time we run 79 | 80 | 81 | *********** v8halp: 82 | 83 | get v8halp to handle & report errors from loading the file 84 | (not just from running each /// line) 85 | 86 | v8halp: prettyprint objects (not just [object Object]) 87 | 88 | 89 | *********** ghcihalp: 90 | 91 | fix: ghcihalp.py isn't reporting errors (or anything else) on this input: 92 | myzip [] [] = [] 93 | myzip (x:xs) (x:ys) = (x, y) : (myzip xs ys) 94 | --- myzip "hello" "world" 95 | 96 | for Haskell, handle compiler error messages for example lines 97 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/README.py: -------------------------------------------------------------------------------- 1 | # <> Next: <> 2 | """ 3 | Regular expressions, how do they work? I'll show you with Python code 4 | you can run and modify right in your editor as you read about it. For 5 | example: 6 | """ 7 | 8 | ## fgrep(r'cat|dog|whale', ['My elephant is sad because', 'my cat has fleas.']) 9 | #. ['my cat has fleas.'] 10 | 11 | def fgrep(pattern, lines): 12 | "Return the lines that match pattern." 13 | strings = pattern.split('|') 14 | return [line for line in lines if any(string in line for string in strings)] 15 | 16 | """ 17 | Change some of this code and hit M-i; the output will change 18 | accordingly. You can put it back then, if you want, using Emacs's 19 | Undo command, but you're encouraged to mess it up, add notes, try your 20 | own ideas, instead. 21 | 22 | There's more on the mechanics of Halp at <<../sample.py>>. 23 | 24 | In this essay we'll survey regular expressions and how to match them 25 | in several ways: naively, by backtracking like classic Unix grep, with 26 | NFAs like Ken Thompson, with Brzozowski derivatives like the Ragel 27 | state machine compiler; and by variations of these. We'll draw out the 28 | connections between the methods and try to make it natural to invent 29 | them. 30 | 31 | I'm going to assume you're pretty good with Python and can get around 32 | in Emacs. So let's go: <> 33 | """ 34 | # <> Next: <> 35 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/TOC.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an active essay best read using Halp. Each module name listed 3 | below should appear in your Emacs buffer as a hyperlink; click on the 4 | first to start reading and playing with it. 5 | 6 | <> 7 | <> 8 | <> 9 | <> 10 | <> 11 | """ 12 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/trivial_match.py: -------------------------------------------------------------------------------- 1 | # <> Prev: <> Next: <<>> 2 | """ 3 | Given matching_strings() we can now write a very simple, albeit 4 | inefficient, match function: 5 | """ 6 | 7 | from whats_a_regex_soln import * 8 | 9 | def match(re, input): 10 | "Does input match re? Return a boolean." 11 | TBD 12 | 13 | """ 14 | (If you find yourself writing supporting functions, you're making it 15 | too complicated. My code: <>) 16 | """ 17 | 18 | ## match(empty, '') 19 | #. True 20 | ## match(empty, 'A') 21 | #. False 22 | ## match(lit('x'), '') 23 | #. False 24 | ## match(lit('x'), 'y') 25 | #. False 26 | ## match(lit('x'), 'x') 27 | #. True 28 | ## match(lit('x'), 'xx') 29 | #. False 30 | ## match(seq(lit('a'), lit('b')), '') 31 | #. False 32 | ## match(seq(lit('a'), lit('b')), 'ab') 33 | #. True 34 | ## match(alt(lit('a'), lit('b')), 'b') 35 | #. True 36 | ## match(alt(lit('a'), lit('b')), 'a') 37 | #. True 38 | ## match(alt(lit('a'), lit('b')), 'x') 39 | #. False 40 | ## match(many(lit('a')), '') 41 | #. True 42 | ## match(many(lit('a')), 'a') 43 | #. True 44 | ## match(many(lit('a')), 'x') 45 | #. False 46 | ## match(many(lit('a')), 'aa') 47 | #. True 48 | ## complicated = seq(many(alt(seq(lit('a'), lit('b')), seq(lit('a'), seq(lit('x'), lit('y'))))), lit('z')) 49 | ## match(complicated, '') 50 | #. False 51 | ## match(complicated, 'z') 52 | #. True 53 | ## match(complicated, 'abz') 54 | #. True 55 | ## match(complicated, 'ababaxyab') 56 | #. False 57 | ## match(complicated, 'ababaxyabz') 58 | #. True 59 | ## match(complicated, 'ababaxyaxz') 60 | #. False 61 | 62 | # <> Prev: <> Next: <<>> 63 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/trivial_match_soln.py: -------------------------------------------------------------------------------- 1 | # <> Prev: <> Next: <<>> 2 | """ 3 | WRITEME 4 | """ 5 | 6 | from whats_a_regex_soln import * 7 | 8 | def match(re, input): 9 | "Does input match re? Return a boolean." 10 | return input in matching_strings(re, len(input)) 11 | 12 | ## match(empty, '') 13 | #. True 14 | ## match(empty, 'A') 15 | #. False 16 | ## match(lit('x'), '') 17 | #. False 18 | ## match(lit('x'), 'y') 19 | #. False 20 | ## match(lit('x'), 'x') 21 | #. True 22 | ## match(lit('x'), 'xx') 23 | #. False 24 | ## match(seq(lit('a'), lit('b')), '') 25 | #. False 26 | ## match(seq(lit('a'), lit('b')), 'ab') 27 | #. True 28 | ## match(alt(lit('a'), lit('b')), 'b') 29 | #. True 30 | ## match(alt(lit('a'), lit('b')), 'a') 31 | #. True 32 | ## match(alt(lit('a'), lit('b')), 'x') 33 | #. False 34 | ## match(many(lit('a')), '') 35 | #. True 36 | ## match(many(lit('a')), 'a') 37 | #. True 38 | ## match(many(lit('a')), 'x') 39 | #. False 40 | ## match(many(lit('a')), 'aa') 41 | #. True 42 | ## complicated = seq(many(alt(seq(lit('a'), lit('b')), seq(lit('a'), seq(lit('x'), lit('y'))))), lit('z')) 43 | ## match(complicated, '') 44 | #. False 45 | ## match(complicated, 'z') 46 | #. True 47 | ## match(complicated, 'abz') 48 | #. True 49 | ## match(complicated, 'ababaxyab') 50 | #. False 51 | ## match(complicated, 'ababaxyabz') 52 | #. True 53 | ## match(complicated, 'ababaxyaxz') 54 | #. False 55 | 56 | # <> Prev: <> Next: <<>> 57 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/whats_a_regex.py: -------------------------------------------------------------------------------- 1 | # <> Prev: <> Next: <> 2 | """ 3 | You've probably used regular expressions via Python's 're' module, 4 | grep, etc. We'll build them from scratch, but skipping, for now, the 5 | bells and whistles that'd distract from the core ideas. 6 | 7 | A regular expression denotes a set of strings. We have five ways to 8 | build one: 9 | 10 | Concrete syntax Example Constructor 11 | =============== ======= =========== 12 | re ::= r'' empty 13 | | r'A' lit(c) 14 | | re '|' re r'X|Y' alt(re1, re2) 15 | | re re r'Hello' seq(re1, re2) 16 | | re '*' r'A*' many(re) 17 | 18 | 'empty' matches the empty string. 19 | 20 | lit(c) matches the single-character string c. (Longer literals can be 21 | matched using seq, below.) 22 | 23 | seq(re1, re2) matches any string with a head matching re1 and a tail 24 | matching re2, with the head immediately abutting the tail. For 25 | example, seq(lit('a'), lit('b')) matches 'ab' and nothing else. re1 26 | and re2 must themselves be regular expressions. (We'll assume the same 27 | by convention for variables named like 're' from now on implicitly.) 28 | 29 | alt(re1, re2) matches any string that matches either of re1 or re2. 30 | 31 | many(re) matches when 0 or more copies of re in sequence would 32 | match. For example, many(lit('A')) matches '', 'A', 'AA', 'AAA', and 33 | so on. 34 | 35 | Define a function matching_strings(re, length) that returns a set of 36 | all of re's matching strings of exactly the given length. Here's a 37 | skeleton and test cases to work from. 38 | """ 39 | 40 | def matching_strings(re, length): 41 | pass 42 | 43 | empty = 'TBD' 44 | 45 | def lit(c): 46 | TBD 47 | 48 | def alt(re1, re2): 49 | TBD 50 | 51 | def seq(re1, re2): 52 | TBD 53 | 54 | def many(re): 55 | TBD 56 | 57 | ## def gen(re, length): return sorted(matching_strings(re, length)) 58 | 59 | ## gen(empty, 0) 60 | #. [''] 61 | ## gen(empty, 1) 62 | #. [] 63 | ## gen(lit('A'), 0) 64 | #. [] 65 | ## gen(lit('A'), 1) 66 | #. ['A'] 67 | ## gen(lit('A'), 2) 68 | #. [] 69 | ## gen(alt(empty, lit('B')), 0) 70 | #. [''] 71 | ## gen(alt(empty, lit('B')), 1) 72 | #. ['B'] 73 | ## gen(alt(lit('A'), lit('B')), 0) 74 | #. [] 75 | ## gen(alt(lit('A'), lit('B')), 1) 76 | #. ['A', 'B'] 77 | ## gen(seq(empty, empty), 0) 78 | #. [''] 79 | ## gen(seq(empty, lit('B')), 0) 80 | #. [] 81 | ## gen(seq(empty, lit('B')), 1) 82 | #. ['B'] 83 | ## gen(seq(empty, lit('B')), 2) 84 | #. [] 85 | ## gen(seq(lit('A'), lit('B')), 2) 86 | #. ['AB'] 87 | ## gen(seq(alt(lit('A'), lit('C')), lit('B')), 2) 88 | #. ['AB', 'CB'] 89 | ## gen(many(empty), 0) 90 | #. [''] 91 | ## gen(many(empty), 1) 92 | #. [] 93 | ## gen(many(lit('A')), 0) 94 | #. [''] 95 | ## gen(many(lit('A')), 1) 96 | #. ['A'] 97 | ## gen(many(lit('A')), 5) 98 | #. ['AAAAA'] 99 | ## gen(seq(lit('A'), seq(many(alt(lit('B'), lit('C'))), lit('D'))), 5) 100 | #. ['ABBBD', 'ABBCD', 'ABCBD', 'ABCCD', 'ACBBD', 'ACBCD', 'ACCBD', 'ACCCD'] 101 | 102 | """ 103 | You can compare to my solution at <>. 104 | 105 | WRITEME exhortation to actually try and write the code yourself first, 106 | with quotes from Feynman, example of Turing, etc. Much of the point of 107 | this new medium of live literate code, I'm supposing. 108 | """ 109 | # <> Prev: <> Next: <> 110 | -------------------------------------------------------------------------------- /examples/learn-the-hell-out-of-regular-expressions/whats_a_regex_soln.py: -------------------------------------------------------------------------------- 1 | # <> Back: <> Next: <> 2 | """ 3 | WRITEME 4 | """ 5 | 6 | def matching_strings(re, length): 7 | return re(length) 8 | 9 | def lit(c): 10 | return lambda n: [c] if n == 1 else [] 11 | 12 | def alt(re1, re2): 13 | return lambda n: re1(n) + re2(n) 14 | 15 | def empty(n): 16 | return [''] if n == 0 else [] 17 | 18 | def seq(re1, re2): 19 | def me(n): 20 | return [s1+s2 for i in range(n+1) for s1 in re1(i) for s2 in re2(n-i)] 21 | return me 22 | 23 | def many(re): 24 | def me(n): 25 | if n == 0: return [''] 26 | return [s1+s2 for i in range(1, n+1) for s1 in re(i) for s2 in me(n-i)] 27 | return me 28 | 29 | 30 | ## def gen(re, length): return sorted(matching_strings(re, length)) 31 | 32 | ## gen(empty, 0) 33 | #. [''] 34 | ## gen(empty, 1) 35 | #. [] 36 | ## gen(lit('A'), 0) 37 | #. [] 38 | ## gen(lit('A'), 1) 39 | #. ['A'] 40 | ## gen(lit('A'), 2) 41 | #. [] 42 | ## gen(alt(empty, lit('B')), 0) 43 | #. [''] 44 | ## gen(alt(empty, lit('B')), 1) 45 | #. ['B'] 46 | ## gen(alt(lit('A'), lit('B')), 0) 47 | #. [] 48 | ## gen(alt(lit('A'), lit('B')), 1) 49 | #. ['A', 'B'] 50 | ## gen(seq(empty, empty), 0) 51 | #. [''] 52 | ## gen(seq(empty, lit('B')), 0) 53 | #. [] 54 | ## gen(seq(empty, lit('B')), 1) 55 | #. ['B'] 56 | ## gen(seq(empty, lit('B')), 2) 57 | #. [] 58 | ## gen(seq(lit('A'), lit('B')), 2) 59 | #. ['AB'] 60 | ## gen(seq(alt(lit('A'), lit('C')), lit('B')), 2) 61 | #. ['AB', 'CB'] 62 | ## gen(many(empty), 0) 63 | #. [''] 64 | ## gen(many(empty), 1) 65 | #. [] 66 | ## gen(many(lit('A')), 0) 67 | #. [''] 68 | ## gen(many(lit('A')), 1) 69 | #. ['A'] 70 | ## gen(many(lit('A')), 5) 71 | #. ['AAAAA'] 72 | ## gen(seq(lit('A'), seq(many(alt(lit('B'), lit('C'))), lit('D'))), 5) 73 | #. ['ABBBD', 'ABBCD', 'ABCBD', 'ABCCD', 'ACBBD', 'ACBCD', 'ACCBD', 'ACCCD'] 74 | 75 | # <> Back: <> Next: <> 76 | -------------------------------------------------------------------------------- /examples/perfectmedians.lhs: -------------------------------------------------------------------------------- 1 | Here's a problem from Brian Hayes's Computing Science column. 2 | 3 | A perfect median of a sequence of consecutive integers is an element 4 | where the sum of the preceding elements equals the sum of the 5 | following ones. 6 | 7 | > isPerfectMedian m n = sum [1..m-1] == sum [m+1..n] 8 | 9 | --- isPerfectMedian 6 8 10 | -- | True 11 | --- isPerfectMedian 7 9 12 | -- | False 13 | 14 | Let's try finding some, in a really stupid way, to start. 15 | 16 | > findMediansSlowly limit = 17 | > [(m, n) | n <- [1..limit], m <- [1..n-1], isPerfectMedian m n] 18 | 19 | --- findMediansSlowly 50 20 | -- | [(6,8),(35,49)] 21 | 22 | OK, a little bit cleverer now: 23 | 24 | > faster limit = 25 | > concat $ map (checkMedian limit) [2..limit] 26 | 27 | > checkMedian limit m = 28 | > let below = ((m-1) * m) `div` 2 29 | > (n, above) = findAbove m below 30 | > in if below == above 31 | > then [(m, n)] 32 | > else [] 33 | 34 | --- faster 500 35 | -- | [(6,8),(35,49),(204,288)] 36 | 37 | > findAbove m below = 38 | > head $ dropWhile (\ (i, s) -> s < below) 39 | > (iterate (\ (i, s) -> (i+1, s+i+1)) (m, 0)) 40 | 41 | --- findAbove 6 15 42 | -- | (8,15) 43 | 44 | (Pardon the horrible code; even I can write better, but the idea was 45 | (to play with Halp as a tool and not focus on code quality.) 46 | -------------------------------------------------------------------------------- /examples/sample.el: -------------------------------------------------------------------------------- 1 | ;; Expressions followed by ';;. ' get results placed there. 2 | ;; Try running M-x halp-update-emacs-lisp now. 3 | 4 | (+ 2 3) ;;. 5 | 6 | (defun hotpo (n) 7 | "Silly example function: half or triple plus one." 8 | (if (evenp n) 9 | (/ n 2) 10 | (1+ (* 3 n)))) 11 | 12 | (hotpo 5) ;;. 13 | (hotpo 6) ;;. 14 | -------------------------------------------------------------------------------- /examples/sample.js: -------------------------------------------------------------------------------- 1 | // Hello, this is a sample. 2 | 3 | function fact(n) { 4 | if (n <= 0) 5 | return 1; 6 | else 7 | return n * fact(n - 1); 8 | } 9 | 10 | // When you hit M-i you should see '//. 120' appear below the following line: 11 | 12 | /// fact(2 + 3) 13 | 14 | // OK. 15 | -------------------------------------------------------------------------------- /examples/sample.lhs: -------------------------------------------------------------------------------- 1 | Hello, this is a sample. 2 | 3 | > fact n = product [1..n] 4 | 5 | When you hit M-i you should see '-- | 120' appear below the following line: 6 | 7 | --- fact (2 + 3) 8 | 9 | OK. GHCi can do other tricks: 10 | 11 | --- :type fact 12 | --- :t fact 13 | -------------------------------------------------------------------------------- /examples/sample.py: -------------------------------------------------------------------------------- 1 | # To use Halp, edit an ordinary Python source file like this one. 2 | 3 | def fact(n): 4 | if n <= 0: 5 | return 1 6 | else: 7 | return n * fact(n - 1) 8 | 9 | # Given comments starting with '## ' like this: 10 | ## 2 + 3 11 | 12 | # Halp will evaluate them when you hit M-i, and insert their value 13 | # below them in another comment (but prefixed with '#. ' instead. Try 14 | # it now. You should see '#. 5' appear below the line above, and 15 | # '#. 120' appear below the following line: 16 | 17 | ## fact(2 + 3) 18 | 19 | # If you want, you can make those lines go away by hitting Undo. But 20 | # you need not; since the outputs are comments they can be left in 21 | # place, where the next M-i will replace them with any new results. 22 | 23 | # Your ## lines can be statements, too: 24 | 25 | ## print('hello\nworld') 26 | 27 | # If they raise exceptions, a traceback appears: 28 | 29 | ## fact('notanumber') 30 | 31 | # If there's an error loading this source file (never mind the ## 32 | # lines) you'll get an error message inserted right at the point of 33 | # the error, and no attempt to evaluate ## lines. You can see this 34 | # happening by uncommenting the following line: 35 | 36 | #foo 37 | 38 | # Finally, you can make the output a function of the previous 39 | # output using halp.read(). This makes it possible to make a 40 | # program's source code the 'user interface' to the program. 41 | 42 | ## 2 * int(halp.read()) 43 | #. 1 44 | -------------------------------------------------------------------------------- /examples/sample.sh: -------------------------------------------------------------------------------- 1 | When you hit M-i you should see '| 5' appear below the following line: 2 | 3 | $ awk 'BEGIN { print 2+3 }' 4 | 5 | Yay! 6 | -------------------------------------------------------------------------------- /examples/sokoban.py: -------------------------------------------------------------------------------- 1 | """ 2 | Prototype of a Sokoban game. It's missing the main program that 3 | would load a starting board, read keys, update the screen, etc.; but 4 | you can still test it interactively inside Halp, and that's how I 5 | developed it. Here we see it at the point it's working and I've played 6 | a whole game through. (And then I simplified the code.) 7 | """ 8 | 9 | # Read an initial board state, and display it -- should leave it unchanged: 10 | 11 | ## print unparse(parse(halp.read())), 12 | #. # # # # # # # 13 | #. # . i # # 14 | #. # o @ o # 15 | #. # o # 16 | #. # . . # 17 | #. # @ # 18 | #. # # # # # # # 19 | 20 | # Below, I play through a game. At first the board was a copy of the 21 | # initial board above, and the keystrokes line below was empty. Then I 22 | # added a keystroke ('d') to the keystrokes line and hit M-i to 23 | # evaluate, and it updated the board. Continuing with more keystrokes 24 | # (making it 'dd', then 'ddl', 'ddlu', ...) and hitting M-i after each 25 | # addition, I played the game (albeit clumsily). After the last M-i, 26 | # the final line further below changed to 'WIN'. (You can keep hitting 27 | # M-i with no change now because 'd' for down is blocked in this 28 | # position.) 29 | 30 | ## keystrokes = halp.read(); print keystrokes, 31 | #. ddlurrddrruullrdrdl 32 | ## board = transform(keystrokes, halp.read()); print board, 33 | #. # # # # # # # 34 | #. # @ # # 35 | #. # @ # 36 | #. # # 37 | #. # @ @ i # 38 | #. # @ # 39 | #. # # # # # # # 40 | 41 | ## print ('' if 'o' in board else 'WIN'), 42 | #. WIN 43 | 44 | def transform(keystrokes, board): 45 | "Scaffolding for the above Halp stuff." 46 | b = parse(board) 47 | push(b, cmds[keystrokes[-1:]]) 48 | return unparse(b) 49 | 50 | # The Sokoban program proper 51 | 52 | def parse(board): 53 | lines = [line.strip() for line in board.splitlines()] 54 | assert lines and all(len(line) == len(lines[0]) for line in lines) 55 | return len(lines[0]), list(''.join(lines)) 56 | 57 | def unparse((width, grid)): 58 | return '\n'.join(''.join(grid[i:i+width]) 59 | for i in range(0, len(grid), width)) 60 | 61 | def up ((width, grid)): return -width 62 | def down ((width, grid)): return width 63 | def left ((width, grid)): return -2 64 | def right((width, grid)): return 2 65 | 66 | cmds = dict(u=up, d=down, l=left, r=right) 67 | 68 | def push((width, grid), direction): 69 | "Update board, trying to move the player in the direction." 70 | i = find_me(grid) 71 | d = direction((width, grid)) 72 | move(grid, 'o@', i+d, i+d+d) 73 | move(grid, 'iI', i, i+d) 74 | 75 | def find_me(grid): 76 | "Return the player's index in the board's array." 77 | return grid.index('i' if 'i' in grid else 'I') 78 | 79 | def move(grid, thing, src, dst): 80 | "Move thing from src to dst if possible." 81 | # N.B. dst is always in bounds when grid[src] in thing because our 82 | # boards have '#'-borders. 83 | if grid[src] in thing and grid[dst] in ' .': 84 | clear(grid, src) 85 | drop(grid, dst, thing) 86 | 87 | def clear(grid, i): 88 | "Remove any thing (box or player) from position i." 89 | grid[i] = ' .'[grid[i] in '.@I'] 90 | 91 | def drop(grid, i, thing): 92 | "At a clear position, put thing." 93 | grid[i] = thing['.' == grid[i]] 94 | -------------------------------------------------------------------------------- /ghci-halp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2006 by Darius Bacon and Brandon Moore 3 | # Distributed under the terms of the MIT X License, found at 4 | # http://www.opensource.org/licenses/mit-license.php 5 | 6 | work=`mktemp -d /tmp/halp.XXXXXXXXXX` 7 | touch ${work}/line_numbers 8 | 9 | sed 's/module.*?where/module Main where/' | grep -v '^| ' >${work}/Main.lhs 10 | echo ' 11 | 12 | > aouhtnuoeahn = 0 -- Ensure file has *some* code.' >>${work}/Main.lhs 13 | 14 | awk '/^[)]/ { print NR >"'${work}/line_numbers'"; 15 | print substr($0, 2); }' ${work}/Main.lhs | 16 | ghci ${work}/Main.lhs 2>&1 | 17 | awk ' 18 | NR <= 4 { next; } 19 | /Leaving GHCi[.]/ { next; } 20 | { sub(/[*]Main> /, "| "); print; } ' | 21 | awk ' 22 | { getline linenumber <"'${work}/line_numbers'"; 23 | print linenumber "a \\"; 24 | print $0; }' >${work}/edits 25 | 26 | sed -f ${work}/edits <${work}/Main.lhs | 27 | # XXX This assumes the original sourcefile ended in a newline character: 28 | awk '{ line[NR] = $0; }; END { for (i = 1; i <= NR-3; ++i) print line[i]; }' 29 | 30 | rm -r ${work} 31 | -------------------------------------------------------------------------------- /ghcihalp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run a Halp-extended .lhs sourcefile from stdin; write to stdout the 4 | same sourcefile with evaluation results placed inline. 5 | """ 6 | 7 | import os 8 | import re 9 | import subprocess 10 | import sys 11 | import tempfile 12 | 13 | dbg = False 14 | 15 | ext = sys.argv[1] 16 | 17 | input = [line for line in sys.stdin if not line.startswith('-- | ')] 18 | if input and not input[-1].endswith('\n'): 19 | input[-1] += '\n' 20 | input.append('\n') 21 | if ext == '.hs': 22 | input.append('aouhtnuoeahn = 0 -- Make sure the file has *some* code.\n') 23 | else: 24 | input.append('> aouhtnuoeahn = 0 -- Make sure the file has *some* code.\n') 25 | 26 | module_name = 'Main' 27 | defn_lines = [] 28 | eval_line_numbers = [] 29 | eval_lines = [] 30 | for i, line in enumerate(input): 31 | if line.startswith('--- '): 32 | eval_line_numbers.append(i+1) 33 | eval_lines.append(line[len('--- '):]) 34 | else: 35 | m = re.search(r'module (.*) where', line) # TODO: more specific 36 | if m: 37 | module_name = m.group(1) 38 | defn_lines.append(line) 39 | 40 | if dbg: 41 | print eval_line_numbers 42 | print ''.join(eval_lines) 43 | 44 | fd, main_lhs = tempfile.mkstemp(ext) 45 | try: 46 | os.write(fd, ''.join(defn_lines)) 47 | os.close(fd) 48 | ghci = subprocess.Popen(['ghci', main_lhs], 49 | stdin=subprocess.PIPE, 50 | stdout=subprocess.PIPE, 51 | stderr=subprocess.STDOUT) # for now 52 | ghci.stdin.write(''.join(eval_lines)) 53 | results, foo = ghci.communicate() 54 | finally: 55 | os.unlink(main_lhs) 56 | 57 | if dbg: 58 | print results, foo 59 | print '' 60 | 61 | def count(it): 62 | n = 0 63 | for flag in it: 64 | if flag: 65 | n += 1 66 | return n 67 | 68 | prompt = '*%s> ' % module_name 69 | result_lines = results.split('\n') 70 | output = input[:-2] 71 | for j, r in enumerate(result_lines): 72 | m = re.search(r'[.]lhs:(\d+):(\d+):', r) 73 | if m: 74 | error_line_num = int(m.group(1)) 75 | output_line_num = (error_line_num 76 | + count(lnum < error_line_num 77 | for lnum in eval_line_numbers)) 78 | output.insert(output_line_num, '-- | At column %s:\n' % m.group(2)) 79 | output_line_num += 1 80 | for plaint_line in result_lines[j+1:]: 81 | if plaint_line.startswith('Failed, modules loaded:'): break 82 | output.insert(output_line_num, '-- | %s\n' % plaint_line) 83 | output_line_num += 1 84 | break 85 | if r.startswith(prompt): 86 | i = 0 87 | for r in result_lines[j:]: 88 | if r.startswith(prompt): 89 | result = r[len(prompt):] 90 | if result.startswith('Leaving GHCi.'): break 91 | output.insert(eval_line_numbers[i] + i, '-- | %s\n' % result) 92 | i += 1 93 | break 94 | 95 | sys.stdout.write(''.join(output).replace('\r\n', '\n')) 96 | -------------------------------------------------------------------------------- /halp.el: -------------------------------------------------------------------------------- 1 | ;; Copyright 2006, 2008 Darius Bacon 2 | ;; Distributed under the terms of the MIT X License, found at 3 | ;; http://www.opensource.org/licenses/mit-license.php 4 | 5 | ;; If for some reason you move the helper programs like pyhalp.py to a 6 | ;; different directory (not the one this file is loaded from) then set 7 | ;; this variable: 8 | (defvar halp-helpers-directory nil 9 | "Directory where Halp helper scripts are installed.") 10 | 11 | (defvar halp-python-command "python" 12 | "Name of the Python program on your system.") 13 | 14 | 15 | ;; The rest of this file shouldn't need editing. 16 | 17 | (require 'cl) 18 | 19 | (defun halp-add-all-hooks () 20 | (halp-add-hook 'sh-mode-hook 'sh-mode-map "\M-i" 'halp-update-sh) 21 | ; Python mode might be called either py-mode or python-mode: 22 | (halp-add-hook 'py-mode-hook 'py-mode-map "\M-i" 'halp-update-python) 23 | (halp-add-hook 'python-mode-hook 'python-mode-map "\M-i" 'halp-update-python) 24 | (halp-add-hook 'haskell-mode-hook 'haskell-mode-map "\M-i" 25 | 'halp-update-haskell) 26 | (halp-add-hook 'literate-haskell-mode-hook 'literate-haskell-mode-map "\M-i" 27 | 'halp-update-literate-haskell) 28 | (halp-add-hook 'javascript-mode-hook 'javascript-mode-map "\M-i" 29 | 'halp-update-javascript) 30 | (halp-add-hook 'js-mode-hook 'js-mode-map "\M-i" 31 | 'halp-update-javascript) 32 | ; (halp-add-hook 'emacs-lisp-mode-hook 33 | ; 'emacs-lisp-mode-map "\M-i" 34 | ; 'halp-update-emacs-lisp) 35 | ) 36 | 37 | (defun halp-add-hook (hook map-name key halp-update-function) 38 | (add-hook hook 39 | `(lambda () 40 | (halp-buttonize-buffer) 41 | (define-key ,map-name ',key ',halp-update-function)))) 42 | 43 | (defun halp-update-sh () 44 | (interactive) 45 | (halp-update-relative "sh-halp.sh" '())) 46 | 47 | (defun halp-update-python () 48 | (interactive) 49 | (halp-find-helpers-directory) 50 | (halp-py-update/diff (concat halp-helpers-directory "pyhalp.py") 51 | (list (buffer-name) (buffer-file-name)))) 52 | 53 | (defun halp-update-javascript () 54 | (interactive) 55 | (halp-find-helpers-directory) 56 | (halp-py-update/diff (concat halp-helpers-directory "v8halp.py") '())) 57 | 58 | (defun halp-update-haskell () 59 | (interactive) 60 | (halp-py-update-relative "ghcihalp.py" '(".hs"))) 61 | 62 | (defun halp-update-literate-haskell () 63 | (interactive) 64 | (halp-py-update-relative "ghcihalp.py" '(".lhs"))) 65 | 66 | (defun halp-py-update-relative (command args) 67 | (halp-find-helpers-directory) 68 | (halp-update halp-python-command 69 | (cons (concat halp-helpers-directory command) args))) 70 | 71 | (defun halp-update-relative (command args) 72 | (halp-find-helpers-directory) 73 | (halp-update (concat halp-helpers-directory command) args)) 74 | 75 | (defun halp-find-helpers-directory () 76 | "Make halp-helpers-directory point to the directory it was 77 | loaded from, if it's not yet initialized." 78 | (unless halp-helpers-directory 79 | (let ((filename (symbol-file 'halp-helpers-directory))) 80 | (when filename 81 | (setq halp-helpers-directory 82 | (file-name-directory filename)))))) 83 | 84 | 85 | ;; Running a helper command and applying its output 86 | 87 | (defun halp-update (command args) 88 | "Update the current buffer using an external helper program." 89 | (interactive) 90 | (message "Halp starting...") 91 | (let ((output (halp-get-output-buffer))) 92 | ;; (call-process-region (point-min) (point-max) "cat" t t) 93 | (let ((rc (apply 'call-process-region 94 | (point-min) (point-max) command nil output nil 95 | args))) 96 | (cond ((zerop rc) ;success 97 | (halp-update-current-buffer output) 98 | (message "Halp starting... done")) 99 | ((numberp rc) 100 | (message "Halp starting... helper process failed")) 101 | (t (message rc)))))) 102 | 103 | (defun halp-py-update/diff (command args) 104 | (halp-update/diff halp-python-command (cons command args))) 105 | 106 | (defun halp-update/diff (command args) 107 | "Update the current buffer using an external helper program 108 | that outputs a diff." 109 | (interactive) 110 | (message "Halp starting...") 111 | (let ((output (halp-get-output-buffer))) 112 | (let ((rc (apply 'call-process-region 113 | (point-min) (point-max) command nil output nil 114 | args))) 115 | (cond ((zerop rc) ;success 116 | (let ((status (halp-update-current-buffer/diff output))) 117 | (message (concat "Halp starting... " status)))) 118 | ((numberp rc) 119 | (message "Halp starting... helper process failed")) 120 | (t (message rc)))))) 121 | 122 | (defun halp-get-output-buffer () 123 | "Return an empty buffer dedicated (hopefully) to halp's use." 124 | (let ((output (get-buffer-create "*halp-output*"))) 125 | (save-current-buffer 126 | (set-buffer output) 127 | (erase-buffer)) 128 | output)) 129 | 130 | (defun halp-update-current-buffer (output) 131 | "Update the current buffer using the output buffer." 132 | ;; Currently we just overwrite the original buffer with the output. 133 | ;; You could get the same effect, more easily, by setting 134 | ;; call-process-region's output buffer to t. (Commented out.) But 135 | ;; we'll soon want to update things more intelligently. 136 | (let ((p (point))) 137 | (erase-buffer) 138 | (insert-buffer output) ;XXX change to insert-buffer-substring ? the difference seems to be saving in the mark 139 | (goto-char p))) 140 | 141 | (defun halp-update-current-buffer/diff (output) 142 | (save-excursion 143 | (halp-apply-diff (current-buffer) output))) 144 | 145 | 146 | ;;; Parsing and applying a diff 147 | 148 | (defun halp-apply-diff (to-buffer from-buffer) 149 | (setq halp-argh '()) 150 | (let ((status "ok")) 151 | (save-current-buffer 152 | (set-buffer from-buffer) 153 | (goto-char (point-min)) 154 | (while (not (eobp)) 155 | (multiple-value-bind (lineno n-del start end) (halp-scan-chunk) 156 | (setq status "changed") 157 | (halp-dbg (list 'chunk lineno n-del start end)) 158 | (set-buffer to-buffer) 159 | (goto-line lineno) 160 | (when (and (eobp) (/= (preceding-char) 10)) 161 | ;; No newline at end of buffer; add it. Otherwise the 162 | ;; code below will delete the last line. 163 | (insert-char 10 1)) 164 | (multiple-value-bind (start1 end1) (halp-scan-lines n-del) 165 | (delete-region start1 end1) 166 | (halp-dbg (list 'deleted n-del start1 end1))) 167 | (insert-buffer-substring from-buffer start end) 168 | (set-buffer from-buffer)))) 169 | status)) 170 | 171 | (defun halp-dbg (x) 172 | (setq halp-argh (cons x halp-argh))) 173 | 174 | (defvar halp-argh nil) 175 | 176 | (defun halp-scan-chunk () 177 | (let* ((lineno (halp-scan-number)) 178 | (n-del (halp-scan-number)) 179 | (n-ins (halp-scan-number))) 180 | (forward-line) 181 | (multiple-value-bind (start end) (halp-scan-lines n-ins) 182 | (values lineno n-del start end)))) 183 | 184 | (defun halp-scan-lines (n) 185 | (let ((start (point))) 186 | (forward-line n) 187 | (values start (point)))) 188 | 189 | (defun halp-scan-number () 190 | (string-to-number (halp-scan-word))) 191 | 192 | (defun halp-scan-word () 193 | (let ((start (point))) 194 | (forward-word 1) 195 | (halp-from start))) 196 | 197 | (defun halp-from (start) 198 | (buffer-substring start (point))) 199 | 200 | 201 | ;; Halp for elisp 202 | 203 | (defun halp-update-emacs-lisp () 204 | "Run all Emacs Lisp expressions in a buffer, and where there's 205 | a ;;. comment after one, replace it with one holding the result." 206 | (interactive) 207 | (save-excursion 208 | (goto-char (point-min)) 209 | (let (next-pos) 210 | (while (setq next-pos (scan-sexps (point) 1)) 211 | (goto-char next-pos) 212 | (let ((result (eval (preceding-sexp)))) 213 | (skip-chars-forward " \t\n") 214 | (when (looking-at ";;\\.") 215 | (delete-region (point) (save-excursion (forward-line 1) (point))) 216 | (insert ";;. ") 217 | (let ((standard-output (current-buffer))) 218 | (prin1 result)) 219 | (insert "\n"))))))) 220 | 221 | 222 | ;; Hyperlinks to other files 223 | 224 | (defun halp-buttonize-buffer () 225 | "Turn each <> in the current buffer into a button." 226 | (save-excursion 227 | (goto-char (point-min)) 228 | (while (re-search-forward "<<[^<> ]+>>" nil t) 229 | (make-button (match-beginning 0) 230 | (match-end 0) 231 | :type 'halp-button)))) 232 | 233 | (define-button-type 'halp-button 234 | 'follow-link t 235 | 'action 'halp-button-action) 236 | 237 | (defun halp-button-action (button) 238 | (find-file (buffer-substring (+ (button-start button) 2) 239 | (- (button-end button) 2)))) 240 | 241 | 242 | ;; Wrap-up 243 | 244 | (halp-add-all-hooks) 245 | (provide 'halp) 246 | -------------------------------------------------------------------------------- /pyhalp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run a Halp-extended .py sourcefile from stdin; write to stdout an 4 | encoding of the same sourcefile with evaluation results placed inline. 5 | The encoding is a kind of diff against the input, expected by halp.el. 6 | """ 7 | 8 | # We want this module to work in either Python 2 or 3. Thus this awkwardness: 9 | try: 10 | from cStringIO import StringIO 11 | except ModuleNotFoundError: 12 | from io import StringIO 13 | 14 | import bisect 15 | import difflib 16 | import os 17 | import sys 18 | import traceback 19 | 20 | 21 | # Evaluation 22 | 23 | source_filename = '' # Default 24 | 25 | current_line_number = None 26 | 27 | def halp(module_text): 28 | """Given a module's code as a string, produce the Halp output as a 29 | string.""" 30 | input_lines = module_text.splitlines() 31 | input, old_outputs = strip_old_outputs(input_lines) 32 | env = set_up_globals(Halp(old_outputs)) 33 | output = format_part(eval_module(input, env)) 34 | return diff(output.splitlines(), input_lines) 35 | 36 | def set_up_globals(halp_object): 37 | if source_filename.endswith('.py'): 38 | module_name = source_filename[:-3] 39 | else: 40 | module_name = '' 41 | return {'__name__': module_name, 42 | '__file__': source_filename, 43 | '__doc__': None, 44 | 'halp': halp_object} 45 | 46 | def eval_module(input, module_dict): 47 | """Given a module's code as a list of lines, produce the Halp 48 | output as a 'part'.""" 49 | global current_line_number 50 | current_line_number = None 51 | 52 | # The "+ '\n'" seems to fix a weird bug where we'd get a 53 | # syntax error sometimes if the last line was a '## ' line not 54 | # ending in a newline character. I still don't understand it. 55 | def thunk(): exec('\n'.join(input) + '\n', module_dict) 56 | output, _, exc_info, is_syn = capturing_stdout(thunk) 57 | if exc_info is not None: 58 | lineno = get_lineno(exc_info) 59 | parts = list(map(InputPart, input)) 60 | parts.insert(lineno, format_exception(exc_info)) 61 | else: 62 | parts = [] 63 | for i, line in enumerate(input): 64 | parts.append(InputPart(line)) 65 | if line.startswith('## '): 66 | code = line[len('## '):] 67 | current_line_number = i + 1 68 | parts.extend(eval_line(code, module_dict)) 69 | if output: 70 | parts.append(OutputPart(output)) 71 | return CompoundPart(parts) 72 | 73 | def eval_line(code, module_dict): 74 | """Given a string that may be either an expression or a statement, 75 | evaluate it and return a list of parts for output.""" 76 | output, result, exc_info, is_syn = \ 77 | capturing_stdout(lambda: eval(code, module_dict)) 78 | if exc_info is not None: 79 | # If a line failed to parse as an expression, it might be the 80 | # line was meant as a statement. (Properly we should first try 81 | # to *parse* instead of *eval*, above. XXX Distinguishing them 82 | # in this way instead is a lazy hack which will misbehave in 83 | # rare cases.) 84 | if is_syn: 85 | def thunk(): exec(code, module_dict) 86 | output, result, exc_info, is_syn = capturing_stdout(thunk) 87 | parts = [] 88 | if output: parts.append(OutputPart(output)) 89 | if result is not None: parts.append(OutputPart(repr(result))) 90 | if exc_info is not None: parts.append(format_exception(exc_info)) 91 | return parts 92 | 93 | def capturing_stdout(thunk): 94 | """Run thunk() and return either (output, result, None, None) or 95 | (output, None, exc_info, is_syntax_error) -- the latter if thunk 96 | raised an exception.""" 97 | # XXX ugly interface to preserve tricky exception/traceback 98 | # capture logic to do with stack frames and line numbers. 99 | # Come back to this -- hopefully could be cleaner. 100 | stdout = sys.stdout 101 | sys.stdout = stringio = StringIO() 102 | try: 103 | result = thunk() 104 | except SyntaxError: 105 | return stringio.getvalue(), None, sys.exc_info(), True 106 | except: 107 | return stringio.getvalue(), None, sys.exc_info(), False 108 | else: 109 | return stringio.getvalue(), result, None, None 110 | finally: 111 | sys.stdout = stdout 112 | 113 | ## strip_old_outputs('hello\n#. world\n#. universe'.split('\n')) 114 | #. (['hello'], {1: ['world', 'universe']}) 115 | 116 | def strip_old_outputs(input_lines): 117 | stripped = [] 118 | old_outputs = {} 119 | for line in input_lines: 120 | if line.startswith('#. '): 121 | old_outputs.setdefault(len(stripped), []).append(line[len('#. '):]) 122 | else: 123 | stripped.append(line) 124 | return stripped, old_outputs 125 | 126 | 127 | # Halp "system-call interface" 128 | # This lets you feed back your command's previous output with 'halp.read()'. 129 | 130 | class Halp: 131 | def __init__(self, old_outputs): 132 | self._old_outputs = old_outputs 133 | def read(self): 134 | return '\n'.join(self._old_outputs.get(current_line_number, [])) 135 | 136 | 137 | # Exception capture 138 | 139 | def format_exception(exc, limit=None): 140 | "Like traceback.format_exception() but returning a 'part'." 141 | (etype, value, tb) = exc 142 | exc_lines = traceback.format_exception_only(etype, value) 143 | exc_only = ''.join(exc_lines).rstrip('\n') 144 | items = extract_censored_tb(tb, limit) 145 | if items: 146 | return CompoundPart([OutputPart('Traceback (most recent call last):'), 147 | TracebackPart(items), 148 | OutputPart(exc_only)]) 149 | else: 150 | return OutputPart(exc_only) 151 | 152 | def extract_censored_tb(tb, limit=None): 153 | """Like traceback.extract_tb() but with Halp internals 154 | bowdlerized. (We assume the top-level halp() call is the top of 155 | our traceback.)""" 156 | # [3:] drops the top frames (which are Halp internals). 157 | items = traceback.extract_tb(tb, limit)[3:] 158 | if items and current_line_number: 159 | # The top item came from a '## ' line; fix its line number: 160 | filename, lineno, func_name, text = items[0] 161 | if filename == '' and lineno == 1: # (should always be true) 162 | items[0] = filename, current_line_number, func_name, None 163 | return items 164 | 165 | def get_lineno(exc): 166 | "Return the line number where this exception should be reported." 167 | (etype, value, tb) = exc 168 | if isinstance(value, SyntaxError) and value.filename == '': 169 | return value.lineno 170 | items = traceback.extract_tb(tb) 171 | if items: 172 | filename, lineno, func_name, text = items[-1] 173 | if filename == '': 174 | return lineno 175 | return 0 176 | 177 | 178 | # Formatting output with tracebacks fixed up. 179 | # 180 | # The problem: we want to get useful tracebacks as in an ordinary Python REPL. 181 | # But when we insert outputs and errors into the halped *source* file, the line 182 | # numbers in the tracebacks get out of sync with the source lines. 183 | # 184 | # It'd be nice if the eval/exec above could take inputs whose line 185 | # numbers were already correct; but this implies two subproblems: 186 | # 1. Getting these line numbers into the eval/exec inputs. 187 | # (You could hack it: prefix k-1 newlines to the string we're evaling for line k.) 188 | # 2. What if a traceback from evaluating a `## ` line references 189 | # lines in the overall module which appear *after* the traceback 190 | # (whose length might differ from that of the previous '#. ' output)? 191 | # This problem might be hackable too by re-evaluating and hoping the 192 | # length stays the same, but ehhh forget it. 193 | # 194 | # So, every exec/eval will appear to Python to be starting at line 1, 195 | # and then here we correct the line numbers in the tracebacks. The 196 | # correction pass waits until after all outputs have been collected. 197 | 198 | def format_part(part): 199 | "Return part expanded into a string, with line numbers corrected." 200 | lnmap = LineNumberMap() 201 | part.count_lines(lnmap) 202 | return '\n'.join(part.format(lnmap)) 203 | 204 | class LineNumberMap: 205 | "Tracks line-number changes and applies them to old line numbers." 206 | def __init__(self): 207 | self.input_lines = [] 208 | self.output_positions = [0] # The line numbers where output is inserted 209 | self.fixups = [0] 210 | # self.fixups[i] is the count of all output lines preceding input lines 211 | # numbered in the range 212 | # self.output_positions[i] < lineno <= self.output_positions[i+1] 213 | # Invariant: 214 | # len(self.output_positions) == len(self.fixups) 215 | # self.output_positions is sorted 216 | def add_input_line(self, line): 217 | self.input_lines.append(line) 218 | def get_input_line(self, lineno): 219 | """Tracebacks sometimes have None for the text of a line, 220 | so we have to supply it ourselves.""" 221 | try: return self.input_lines[lineno - 1] 222 | except IndexError: return None 223 | def count_output(self, n_lines): 224 | self.output_positions.append(1 + len(self.input_lines)) 225 | self.fixups.append(self.fixups[-1] + n_lines) 226 | def fix_lineno(self, lineno): 227 | i = bisect.bisect_right(self.output_positions, lineno) - 1 228 | return lineno + self.fixups[i] 229 | 230 | class CompoundPart: 231 | "A part that's a sequence of subparts." 232 | def __init__(self, parts): 233 | self.parts = parts 234 | def count_lines(self, lnmap): 235 | for part in self.parts: 236 | part.count_lines(lnmap) 237 | def format(self, lnmap): 238 | return sum((part.format(lnmap) for part in self.parts), []) 239 | 240 | class InputPart: 241 | "An input line, passed to the output unchanged." 242 | def __init__(self, text): 243 | self.text = text 244 | def count_lines(self, lnmap): 245 | lnmap.add_input_line(self.text) 246 | def format(self, lnmap): 247 | return [self.text] 248 | 249 | class OutputPart: 250 | "Some output lines, with a #. prefix." 251 | def __init__(self, text): 252 | self.lines = text.splitlines() 253 | def count_lines(self, lnmap): 254 | lnmap.count_output(len(self.lines)) 255 | def format(self, lnmap): 256 | return ['#. ' + line for line in self.lines] 257 | 258 | class TracebackPart: 259 | """An output traceback with a #. prefix and with the stack frames 260 | corrected when they refer to the code being halped.""" 261 | def __init__(self, tb_items): 262 | self.items = tb_items 263 | def count_lines(self, lnmap): 264 | def item_len(item): 265 | (filename, lineno, name, line) = item 266 | # XXX how to make sure this count is consistent with format_traceback()? 267 | if line: return 2 268 | else: return 1 269 | lnmap.count_output(sum(map(item_len, self.items))) 270 | def format(self, lnmap): 271 | def fix_item(item): 272 | (filename, lineno, name, line) = item 273 | if filename == '': 274 | filename = source_filename 275 | line = lnmap.get_input_line(lineno) 276 | lineno = lnmap.fix_lineno(lineno) 277 | return (filename, lineno, name, line) 278 | return format_traceback(map(fix_item, self.items)) 279 | 280 | def format_traceback(tb_items): 281 | "Turn a list of traceback items into a string." 282 | return ['#. ' + line.rstrip('\n').replace('\n', '\n#. ') 283 | for line in traceback.format_list(tb_items)] 284 | 285 | 286 | # Producing a diff between input and output 287 | 288 | def diff(new_lines, old_lines): 289 | return format_diff(compute_diff(None, new_lines, old_lines)) 290 | 291 | def format_diff(triples): 292 | return ''.join('%d %d %d\n%s' % (lo+1, hi-lo, len(lines), 293 | ''.join(line + '\n' for line in lines)) 294 | for lines, lo, hi in triples) 295 | 296 | def compute_diff(is_junk, a, b): 297 | """ 298 | Pre: is_junk: None or (string -> bool) 299 | a, b: [string] 300 | Return a list of triples (lines, lo, hi) representing the edits 301 | to convert b to a. The ranges (lo,hi) are disjoint and in 302 | descending order. Setting each b[lo:hi] = lines, in order, yields a. 303 | """ 304 | sm = difflib.SequenceMatcher(is_junk, a, b) 305 | i = j = 0 306 | triples = [] 307 | for ai, bj, size in sm.get_matching_blocks(): 308 | # Invariant: 309 | # triples is the diff for a[:i], b[:j] 310 | # and the next matching block is a[ai:ai+size] == b[bj:bj+size]. 311 | if i < ai or j < bj: 312 | triples.append((a[i:ai], j, bj)) 313 | i, j = ai+size, bj+size 314 | triples.reverse() 315 | return triples 316 | 317 | 318 | # Main program 319 | 320 | if __name__ == '__main__': 321 | if 2 <= len(sys.argv): source_filename = sys.argv[1] 322 | if 3 <= len(sys.argv): sys.path[0] = os.path.dirname(sys.argv[2]) 323 | sys.stdout.write(halp(sys.stdin.read())) 324 | -------------------------------------------------------------------------------- /sh-halp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2006 by Darius Bacon and Brandon Moore 3 | # Distributed under the terms of the MIT X License, found at 4 | # http://www.opensource.org/licenses/mit-license.php 5 | 6 | work=`mktemp -d /tmp/halp.XXXXXXXXXX` 7 | touch ${work}/line_numbers 8 | 9 | grep -v '^| ' >${work}/input 10 | awk '/^[$]/ { print NR >"'${work}/line_numbers'"; 11 | print "echo -n SEP;" substr($0, 2); }' ${work}/input | 12 | sh | sed 's/^SEP/| /' | 13 | awk ' 14 | { getline linenumber <"'${work}/line_numbers'"; 15 | print linenumber "a \\"; 16 | print $0; }' >${work}/edits 17 | 18 | sed -f ${work}/edits <${work}/input 19 | 20 | rm -r ${work} 21 | -------------------------------------------------------------------------------- /tests/expected.sample.py: -------------------------------------------------------------------------------- 1 | 43 1 1 2 | #. 2 3 | 30 0 4 4 | #. Traceback (most recent call last): 5 | #. File "", line 7, in fact 6 | #. return n * fact(n - 1) 7 | #. TypeError: unsupported operand type(s) for -: 'str' and 'int' 8 | 26 0 3 9 | #. hello 10 | #. world 11 | #. 12 | 18 0 1 13 | #. 120 14 | 11 0 1 15 | #. 5 16 | -------------------------------------------------------------------------------- /tests/test-pyhalp.sh: -------------------------------------------------------------------------------- 1 | python ../pyhalp.py <../examples/sample.py >tmp && 2 | diff -u expected.sample.py tmp && 3 | 4 | python ../pyhalp.py tmp && 5 | diff -u /dev/null tmp && 6 | 7 | true 8 | -------------------------------------------------------------------------------- /v8halp.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2008 Google Inc. All Rights Reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided that the following conditions are 4 | // met: 5 | // 6 | // * Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // * Redistributions in binary form must reproduce the above 9 | // copyright notice, this list of conditions and the following 10 | // disclaimer in the documentation and/or other materials provided 11 | // with the distribution. 12 | // * Neither the name of Google Inc. nor the names of its 13 | // contributors may be used to endorse or promote products derived 14 | // from this software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | 34 | void RunShell(v8::Handle context); 35 | bool ExecuteString(v8::Handle source, 36 | v8::Handle name, 37 | bool print_result); 38 | v8::Handle Print(const v8::Arguments& args); 39 | v8::Handle Load(const v8::Arguments& args); 40 | v8::Handle Quit(const v8::Arguments& args); 41 | v8::Handle Version(const v8::Arguments& args); 42 | v8::Handle ReadFile(const char* name); 43 | void ProcessRuntimeFlags(int argc, char* argv[]); 44 | 45 | 46 | int main(int argc, char* argv[]) { 47 | v8::V8::SetFlagsFromCommandLine(&argc, argv, true); 48 | v8::HandleScope handle_scope; 49 | // Create a template for the global object. 50 | v8::Handle global = v8::ObjectTemplate::New(); 51 | // Bind the global 'print' function to the C++ Print callback. 52 | global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print)); 53 | // Bind the global 'load' function to the C++ Load callback. 54 | global->Set(v8::String::New("load"), v8::FunctionTemplate::New(Load)); 55 | // Bind the 'quit' function 56 | global->Set(v8::String::New("quit"), v8::FunctionTemplate::New(Quit)); 57 | // Bind the 'version' function 58 | global->Set(v8::String::New("version"), v8::FunctionTemplate::New(Version)); 59 | // Create a new execution environment containing the built-in 60 | // functions 61 | v8::Handle context = v8::Context::New(NULL, global); 62 | // Enter the newly created execution environment. 63 | v8::Context::Scope context_scope(context); 64 | RunShell(context); 65 | // Evaluate the command-line argument expressions. 66 | for (int i = 1; i < argc; i++) { 67 | const char* expr = argv[i]; 68 | //fprintf (stderr, "arg: [%s]\n", expr); 69 | v8::HandleScope handle_scope; 70 | ExecuteString(v8::String::New(expr), v8::Undefined(), true); 71 | } 72 | return 0; 73 | } 74 | 75 | 76 | // Executes a string within the current v8 context. 77 | bool ExecuteString(v8::Handle source, 78 | v8::Handle name, 79 | bool print_result) { 80 | v8::HandleScope handle_scope; 81 | v8::TryCatch try_catch; 82 | v8::Handle script = v8::Script::Compile(source, name); 83 | if (script.IsEmpty()) { 84 | // Print errors that happened during compilation. 85 | v8::String::AsciiValue error(try_catch.Exception()); 86 | printf("1\n%s\n", *error); 87 | return false; 88 | } else { 89 | v8::Handle result = script->Run(); 90 | if (result.IsEmpty()) { 91 | // Print errors that happened during execution. 92 | v8::String::AsciiValue error(try_catch.Exception()); 93 | printf("1\n%s\n", *error); 94 | return false; 95 | } else { 96 | if (print_result && result->IsUndefined()) { 97 | printf("0\n"); 98 | } else if (print_result && !result->IsUndefined()) { 99 | // If all went well and the result wasn't undefined then print 100 | // the returned value. 101 | v8::String::AsciiValue str(result); 102 | int nl = 0; 103 | for (int i = 0; (*str)[i]; ++i) { 104 | if ((*str)[i] == '\n') ++nl; 105 | } 106 | printf("%d\n%s\n", nl + 1, *str); 107 | } 108 | return true; 109 | } 110 | } 111 | } 112 | 113 | 114 | // The read-eval-execute loop of the shell. 115 | void RunShell(v8::Handle context) { 116 | static const int kBufferSize = 128*1024; 117 | char buffer[kBufferSize]; 118 | size_t n = fread(buffer, 1, kBufferSize, stdin); 119 | if (n < 0) return; 120 | //fprintf (stderr, "input text: [\n%s]\n", buffer); 121 | v8::HandleScope handle_scope; 122 | ExecuteString(v8::String::New(buffer), v8::Undefined(), false); 123 | } 124 | 125 | 126 | // The callback that is invoked by v8 whenever the JavaScript 'print' 127 | // function is called. Prints its arguments on stdout separated by 128 | // spaces and ending with a newline. 129 | v8::Handle Print(const v8::Arguments& args) { 130 | bool first = true; 131 | for (int i = 0; i < args.Length(); i++) { 132 | v8::HandleScope handle_scope; 133 | if (first) { 134 | first = false; 135 | } else { 136 | printf(" "); 137 | } 138 | v8::String::AsciiValue str(args[i]); 139 | printf("%s", *str); 140 | } 141 | printf("\n"); 142 | return v8::Undefined(); 143 | } 144 | 145 | 146 | // The callback that is invoked by v8 whenever the JavaScript 'load' 147 | // function is called. Loads, compiles and executes its argument 148 | // JavaScript file. 149 | v8::Handle Load(const v8::Arguments& args) { 150 | for (int i = 0; i < args.Length(); i++) { 151 | v8::HandleScope handle_scope; 152 | v8::String::AsciiValue file(args[i]); 153 | v8::Handle source = ReadFile(*file); 154 | if (source.IsEmpty()) { 155 | return v8::ThrowException(v8::String::New("Error loading file")); 156 | } 157 | ExecuteString(source, v8::String::New(*file), false); 158 | } 159 | return v8::Undefined(); 160 | } 161 | 162 | 163 | // The callback that is invoked by v8 whenever the JavaScript 'quit' 164 | // function is called. Quits. 165 | v8::Handle Quit(const v8::Arguments& args) { 166 | // If not arguments are given args[0] will yield undefined which 167 | // converts to the integer value 0. 168 | int exit_code = args[0]->Int32Value(); 169 | exit(exit_code); 170 | return v8::Undefined(); 171 | } 172 | 173 | 174 | v8::Handle Version(const v8::Arguments& args) { 175 | return v8::String::New(v8::V8::GetVersion()); 176 | } 177 | 178 | 179 | // Reads a file into a v8 string. 180 | v8::Handle ReadFile(const char* name) { 181 | FILE* file = fopen(name, "rb"); 182 | if (file == NULL) return v8::Handle(); 183 | 184 | fseek(file, 0, SEEK_END); 185 | int size = ftell(file); 186 | rewind(file); 187 | 188 | char* chars = new char[size + 1]; 189 | chars[size] = '\0'; 190 | for (int i = 0; i < size;) { 191 | int read = fread(&chars[i], 1, size - i, file); 192 | i += read; 193 | } 194 | fclose(file); 195 | v8::Handle result = v8::String::New(chars, size); 196 | delete[] chars; 197 | return result; 198 | } 199 | -------------------------------------------------------------------------------- /v8halp.mk: -------------------------------------------------------------------------------- 1 | v8halp: v8halp.cc libv8.a 2 | g++ -m32 -Iinclude v8halp.cc -o v8halp libv8.a -lpthread 3 | -------------------------------------------------------------------------------- /v8halp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run a Halp-extended .js sourcefile from stdin; write to stdout the 4 | same sourcefile with evaluation results placed inline. 5 | """ 6 | 7 | import difflib 8 | import subprocess 9 | import sys 10 | 11 | 12 | # Evaluation 13 | 14 | def halp(module_text): 15 | """Given a module's code as a string, produce the Halp output as a 16 | string.""" 17 | input_lines = module_text.split('\n') 18 | input = [line for line in input_lines if not line.startswith('//. ')] 19 | output = eval_module(input) 20 | #print 'output', output 21 | return diff(output, input_lines) 22 | 23 | def eval_module(input): 24 | """Given a module's code as a list of lines, produce the Halp 25 | output as a list of lines.""" 26 | halp_lines = [] 27 | halp_linenos = [] 28 | for i, line in enumerate(input): 29 | if line.startswith('/// '): 30 | halp_lines.append(line[len('/// '):]) 31 | halp_linenos.append(i+1) 32 | result_string = call_v8halp('\n'.join(input), halp_lines) 33 | result_lines = result_string.split('\n') 34 | result_chunks = [] 35 | j = 0 36 | #print 'result_lines', result_lines 37 | while j < len(result_lines): 38 | if result_lines[j] == '': break 39 | nlines = int(result_lines[j]) 40 | result_chunks.append(result_lines[j+1:j+1+nlines]) 41 | j += 1 + nlines 42 | #print 'result_chunks', result_chunks 43 | #print 'halp_linenos', halp_linenos 44 | assert len(result_chunks) == len(halp_linenos) 45 | output = list(input) 46 | for lineno, chunk in reversed(zip(halp_linenos, result_chunks)): 47 | output[lineno:lineno] = ['//. ' + line for line in chunk] 48 | return output 49 | 50 | def call_v8halp(text, halp_lines): 51 | #print 'halp_lines', halp_lines 52 | args = ['v8halp'] + halp_lines 53 | p = subprocess.Popen(args, 54 | stdin=subprocess.PIPE, 55 | stdout=subprocess.PIPE, 56 | stderr=None) 57 | stdout, stderr = p.communicate(input=text) 58 | #print 'stdout', repr(stdout) 59 | #print stderr 60 | return stdout 61 | 62 | 63 | # Producing a diff between input and output 64 | 65 | def diff(new_lines, old_lines): 66 | return format_diff(compute_diff(None, new_lines, old_lines)) 67 | 68 | def format_diff(triples): 69 | return ''.join('%d %d %d\n%s' % (lo+1, hi-lo, len(lines), 70 | ''.join(line + '\n' for line in lines)) 71 | for lines, lo, hi in triples) 72 | 73 | def compute_diff(is_junk, a, b): 74 | """ 75 | Pre: is_junk: None or (string -> bool) 76 | a, b: [string] 77 | Return a list of triples (lines, lo, hi) representing the edits 78 | to convert b to a. The ranges (lo,hi) are disjoint and in 79 | descending order. Setting each b[lo:hi] = lines, in order, yields a. 80 | """ 81 | sm = difflib.SequenceMatcher(is_junk, a, b) 82 | i = j = 0 83 | triples = [] 84 | for ai, bj, size in sm.get_matching_blocks(): 85 | # Invariant: 86 | # triples is the diff for a[:i], b[:j] 87 | # and the next matching block is a[ai:ai+size] == b[bj:bj+size]. 88 | if i < ai or j < bj: 89 | triples.append((a[i:ai], j, bj)) 90 | i, j = ai+size, bj+size 91 | triples.reverse() 92 | return triples 93 | 94 | 95 | # Main program 96 | 97 | if __name__ == '__main__': 98 | sys.stdout.write(halp(sys.stdin.read())) 99 | --------------------------------------------------------------------------------