Compare commits

...

2 Commits

Author SHA1 Message Date
7feddbcfbe Extraction from streams (#25)
All checks were successful
continuous-integration/drone/push Build is passing
Add the ! postfix operator and !! expression.
  Also add the ++ string concatenation operator.
  Also allow specification of standard input in the test scheme.

  Resolves #7, #18.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: glen/fostr#25
Co-Authored-By: Glen Whitney <glen@nobody@nowhere.net>
Co-Committed-By: Glen Whitney <glen@nobody@nowhere.net>
2021-03-13 19:30:23 +00:00
cc89ad1e93 Add OCaml code generation (#24)
All checks were successful
continuous-integration/drone/push Build is passing
Also start using nailgun to speed up code generation.

  Resolves #6.

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: glen/fostr#24
Co-Authored-By: Glen Whitney <glen@nobody@nowhere.net>
Co-Committed-By: Glen Whitney <glen@nobody@nowhere.net>
2021-03-01 20:40:35 +00:00
18 changed files with 258 additions and 16 deletions

View File

@ -29,7 +29,18 @@ steps:
- java -jar /drone/lib/spt/org.metaborg.spt.cmd/target/org.metaborg.spt.cmd* -l . -s /drone/lib/spt/org.metaborg.meta.lang.spt -t tests - java -jar /drone/lib/spt/org.metaborg.spt.cmd/target/org.metaborg.spt.cmd* -l . -s /drone/lib/spt/org.metaborg.meta.lang.spt -t tests
- mkdir -p lib - mkdir -p lib
- curl -o lib/sunshine.jar -L 'http://artifacts.metaborg.org/service/local/artifact/maven/redirect?r=snapshots&g=org.metaborg&a=org.metaborg.sunshine2&v=LATEST' - curl -o lib/sunshine.jar -L 'http://artifacts.metaborg.org/service/local/artifact/maven/redirect?r=snapshots&g=org.metaborg&a=org.metaborg.sunshine2&v=LATEST'
- bin/fosgen tests/emit_sum.fos - name: setup_gen
image: gcc
volumes:
- name: m2
path: /root/.m2
commands:
- git clone https://github.com/facebook/nailgun.git
- cd nailgun
- make
- cd ../bin
- ln -s ../nailgun/nailgun-client/target/ng .
- cd ..
- name: extract_tests - name: extract_tests
image: xonsh/xonsh image: xonsh/xonsh
commands: commands:
@ -41,7 +52,8 @@ steps:
path: /drone/lib path: /drone/lib
- name: m2 - name: m2
path: /root/.m2 path: /root/.m2
commands: commands: # Note we first make sure that fosgen is working
- bin/fosgen -d tests/emit_sum.fos
- bin/generate_test_code - bin/generate_test_code
- name: python_tests - name: python_tests
image: python:slim image: python:slim
@ -55,6 +67,13 @@ steps:
image: haskell image: haskell
commands: commands:
- bin/run_tests runghc hs - bin/run_tests runghc hs
- name: ocaml_tests
image: ocaml/opam
commands:
- ls -als tests/extracted
- opam init
- eval $(opam env)
- bin/run_tests ocaml ml
volumes: volumes:
- name: lib - name: lib

6
.gitignore vendored
View File

@ -12,10 +12,16 @@
.pydevproject .pydevproject
a.out
*.aterm *.aterm
/site /site
bin/ng
tests/extracted/* tests/extracted/*
tests/*.js tests/*.js
tests/*.py tests/*.py
tests/*.hs tests/*.hs
tests/*.ml
tests/*.cmi
tests/*.cmo
adhoc* adhoc*

View File

@ -13,12 +13,16 @@ DESTINATION = 'tests/extracted'
# Extension for extracted files: # Extension for extracted files:
EXT = 'fos' EXT = 'fos'
# Extension for desired input:
INP = 'in'
# Extension for expectations: # Extension for expectations:
EXP = 'expect' EXP = 'expect'
for path in TEST_LIST: for path in TEST_LIST:
destdir = pf"{DESTINATION}/{path.stem}" destdir = pf"{DESTINATION}/{path.stem}"
mkdir -p @(destdir) mkdir -p @(destdir)
chmod ugo+rwx @(destdir)
contents = path.read_text() contents = path.read_text()
tests = re.split(r'test\s*(.+?)\s*\[\[.*?\n', contents)[1:] tests = re.split(r'test\s*(.+?)\s*\[\[.*?\n', contents)[1:]
testit = iter(tests) testit = iter(tests)
@ -33,6 +37,11 @@ for path in TEST_LIST:
expath = destdir / f"{name}.{EXT}" expath = destdir / f"{name}.{EXT}"
expath.write_text(example) expath.write_text(example)
echo Wrote @(expath) echo Wrote @(expath)
im = re.search(r'/\*\*\s+accepts.*?\n([\s\S]*?)\*\*/', details[em.end():])
if im:
ipath = destdir / f"{name}.{INP}"
ipath.write_text(im[1])
echo " ...and" @(ipath)
xm = re.search(r'/\*\*\s+writes.*?\n([\s\S]*?)\*\*/', details[em.end():]) xm = re.search(r'/\*\*\s+writes.*?\n([\s\S]*?)\*\*/', details[em.end():])
if xm: if xm:
xpath = destdir / f"{name}.{EXP}" xpath = destdir / f"{name}.{EXP}"

View File

@ -5,6 +5,7 @@ erro() { printf "%s\n" "$*" >&2; }
##### Set defaults: ##### Set defaults:
SUPPRESS_ERR=YES SUPPRESS_ERR=YES
USE_NAILGUN=YES
LANGUAGE=Python LANGUAGE=Python
##### Extract command line options: ##### Extract command line options:
@ -14,18 +15,23 @@ do
-h|--help) -h|--help)
echo echo
echo "Usage:" echo "Usage:"
echo " fosgen [-d] [-l LANGUAGE] FILE" echo " fosgen [-d] [-j] [-l LANGUAGE] FILE"
echo echo
echo "Writes to standard output the code generated from the fostr" echo "Writes to standard output the code generated from the fostr"
echo "program in FILE, targeting the specified LANGUAGE (which" echo "program in FILE, targeting the specified LANGUAGE (which"
echo "defaults to Python)." echo "defaults to Python)."
echo echo
echo "The -d option writes diagnostic output to standard error." echo "The -d option writes diagnostic output to standard error."
echo "The -j option uses the Spoofax Sunshine JAR directly, rather"
echo "than via nailgun."
exit exit
;; ;;
-d) -d)
SUPPRESS_ERR='' SUPPRESS_ERR=''
;; ;;
-j)
USE_NAILGUN=''
;;
-l) -l)
shift shift
LANGUAGE="$1" LANGUAGE="$1"
@ -67,5 +73,17 @@ then
exec 2>/dev/null exec 2>/dev/null
fi fi
if [[ $USE_NAILGUN ]]
then
if [[ $SUPPRESS_ERR ]]
then
$BINDIR/let_sun_shine
else
$BINDIR/let_sun_shine noisy
fi
$BINDIR/ng sunshine transform -p $PROJDIR -n $LANGUAGE -i $PROGRAM
exit $?
fi
java -jar $SUNJAR transform -p $PROJDIR -l $PROJDIR -l $MVN_REPO -n $LANGUAGE -i $PROGRAM java -jar $SUNJAR transform -p $PROJDIR -l $PROJDIR -l $MVN_REPO -n $LANGUAGE -i $PROGRAM
exit $? exit $?

View File

@ -4,7 +4,7 @@ failed=0
for dir in tests/extracted/*; do for dir in tests/extracted/*; do
for file in $dir/*.fos; do for file in $dir/*.fos; do
for language in Python Javascript Haskell; do for language in Python Javascript Haskell OCaml; do
echo bin/fosgen -l ${language%.*} $file ... echo bin/fosgen -l ${language%.*} $file ...
bin/fosgen -l $language $file bin/fosgen -l $language $file
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then

40
bin/let_sun_shine Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
# Helper for fosgen, not intended to be used directly
# With an argument, print diagnostic output
BINDIR=$(dirname $BASH_SOURCE)
if $BINDIR/ng sunshine --help
then
if [[ $1 ]]
then
echo "sun already shining."
fi
else
if [[ $1 ]]
then
echo "disperse the clouds."
fi
SUNJAR="$BINDIR/../lib/sunshine.jar"
PROJDIR="$BINDIR/.."
if [[ ! $MVN_REPO ]]; then
MVN_REPO="$HOME/.m2/repository"
fi
if [[ ! -d $MVN_REPO ]]; then
MVN_REPO="/root/.m2/repository"
fi
if [[ ! -d $MVN_REPO ]]; then
echo "Cannot find your Maven repository. Please set environment variable"
echo "MVN_REPO to its full path and re-run."
exit 1
fi
if [[ $1 ]]
then
java -jar $SUNJAR server &
else
java -jar $SUNJAR server >/dev/null 2>&1 &
fi
sleep 5
$BINDIR/ng sunshine load -l $PROJDIR -l $MVN_REPO
fi

View File

@ -9,8 +9,14 @@ diffed=0
for dir in tests/extracted/*; do for dir in tests/extracted/*; do
for file in $dir/*.$ext; do for file in $dir/*.$ext; do
((total++)) ((total++))
if [[ -f ${file%.*}.in ]]; then
cat ${file%.*}.in | $command $file > $file.out
result=$?
else
$command $file > $file.out $command $file > $file.out
if [[ $? -ne 0 ]]; then result=$?
fi
if [[ $result -ne 0 ]]; then
echo ERROR: $command $file failed. echo ERROR: $command $file failed.
((failed++)) ((failed++))
else else

View File

@ -4,3 +4,4 @@ menus
action: "Python" = to-python action: "Python" = to-python
action: "Javascript" = to-javascript action: "Javascript" = to-javascript
action: "Haskell" = to-haskell action: "Haskell" = to-haskell
action: "OCaml" = to-ocaml

View File

@ -9,7 +9,7 @@ plugins:
- search - search
- semiliterate: - semiliterate:
ignore_folders: [target, lib] ignore_folders: [target, lib]
exclude_extensions: ['.o', '.hi'] exclude_extensions: ['.o', '.hi', '.cmi', '.cmo']
extract_standard_markdown: extract_standard_markdown:
terminate: <!-- /md --> terminate: <!-- /md -->
theme: theme:

View File

@ -42,10 +42,13 @@ context-free syntax
Ex.EscString = STRING Ex.EscString = STRING
Ex.Stream = <stream> Ex.Stream = <stream>
Ex.Sum = <<Ex> + <Ex>> {left} Ex.Sum = <<Ex> + <Ex>> {left}
Ex.Concat = <<Ex> ++ <Ex>> {left}
Ex.Gets = [[Ex] << [Ex]] {left} Ex.Gets = [[Ex] << [Ex]] {left}
Ex.DefGets = [<<< [Ex]] Ex.DefGets = [<<< [Ex]]
Ex.To = [[Ex] >> [Ex]] {left} Ex.To = [[Ex] >> [Ex]] {left}
Ex.DefTo = [[Ex] >>>] Ex.DefTo = [[Ex] >>>]
Ex.Emits = <<Ex>!>
Ex.DefEmits = <!!>
Ex = <(<Ex>)> {bracket} Ex = <(<Ex>)> {bracket}
@ -53,7 +56,7 @@ context-free priorities
Ex.To Ex.To
> Ex.DefTo > Ex.DefTo
> Ex.Sum > {Ex.Sum Ex.Concat}
> Ex.DefGets > Ex.DefGets
> Ex.Gets, > Ex.Gets,

View File

@ -59,8 +59,8 @@ For example, this snippet generates the following Python:
It generates nearly identical code in It generates nearly identical code in
this simple example for Javascript (just with `"Hello, world!"` this simple example for Javascript (just with `"Hello, world!"`
in place of `r'Hello, world!'`), although it generates a different in place of `r'Hello, world!'`), although it generates a different
preamble defining Stdio for each language. (Currently, Haskell code preamble defining Stdio for each language. (Currently, Haskell and OCaml
generation is also supported.) code generation are also supported.)
There's not much to break down in such a tiny program as this, but let's do There's not much to break down in such a tiny program as this, but let's do
it. The prefix operator `<<<` could be read as "the default stream receives...", it. The prefix operator `<<<` could be read as "the default stream receives...",
@ -108,7 +108,7 @@ again. That way we can use the general (left-associative)
parse to TopLevel( parse to TopLevel(
Gets(Gets(Gets(Gets(DefGets(LitString("'Two and '")),Int("2")), Gets(Gets(Gets(Gets(DefGets(LitString("'Two and '")),Int("2")),
LitString("' make '")),Sum(Int("2"),Int("2"))), LitString("' make '")),Sum(Int("2"),Int("2"))),
EscString("\"./n\""))) EscString("\".\n\"")))
/** writes /** writes
Two and 2 make 4. Two and 2 make 4.
**/ **/
@ -293,4 +293,38 @@ test emit_several_default [[
>>> >>>
]] parse succeeds ]] parse succeeds
/** writes /** writes
3399677527121313*/ 3399677527121313**/
/** md
### Streams are bidirectional
So far we have only sent items to a stream. But we can extract them from
streams as well, with the `!` postfix operator. `!!` all by itself abbreviates
`stream!`, i.e., extraction from the standard stream. For example,
```fostr
**/
/** md */ test custom_hw [[
<<< "What is your name?\n"
<<< 'Hello, ' ++ !!
]] /* **/
parse to TopLevel(Sequence([
DefGets(EscString("\"What is your name?\n\"")),
DefGets(Concat(LitString("'Hello, '"),DefEmits()))
]))
/** accepts
Kilroy
**/
/** writes
What is your name?
Hello, Kilroy
**/
/** md
```
queries users for their name and then writes a customized greeting. It also
illustrates the use of `++` for string concatenation, as opposed to `+` for
(numerical) addition.
**/

View File

@ -17,6 +17,7 @@ rules
defStream: DefGets(x) -> Gets(Stream(), x) defStream: DefGets(x) -> Gets(Stream(), x)
defStream: DefTo(x) -> To(x, Stream()) defStream: DefTo(x) -> To(x, Stream())
defStream: DefEmits() -> Emits(Stream())
strategies strategies

View File

@ -6,6 +6,7 @@ imports
pp pp
outline outline
analysis analysis
ocaml
haskell haskell
javascript javascript
python python

View File

@ -10,7 +10,7 @@ rules
rule. rule.
C) We will use bottomup-para to traverse the full AST with the C) We will use bottomup-para to traverse the full AST with the
transformation from A so that we have access to the original expression transformation from A so that we have access to the original expression
(and get get the Statix-associated type when we need to). (and can get the Statix-associated type when we need to).
Hence the transformation in (A) must actually take a pair of Hence the transformation in (A) must actually take a pair of
an (original) term and a term with value strings at each child, an (original) term and a term with value strings at each child,
and be certain to return a value string. and be certain to return a value string.
@ -23,6 +23,7 @@ rules
import System.IO import System.IO
data IOStream = StdIO data IOStream = StdIO
-- Danger: These currently assume the stream is StdIO
gets :: Show b => a -> b -> IO a gets :: Show b => a -> b -> IO a
gets s d = do gets s d = do
putStr(show d) putStr(show d)
@ -33,6 +34,10 @@ rules
putStr(d) putStr(d)
return s return s
emit s = do
l <- getLine
return (l ++ "\n")
main = do main = do
[<Preactions>()]return [val]] [<Preactions>()]return [val]]
@ -41,6 +46,7 @@ rules
hs: (_, LitString(x)) -> <haskLitString>x hs: (_, LitString(x)) -> <haskLitString>x
hs: (_, EscString(x)) -> x hs: (_, EscString(x)) -> x
hs: (_, Sum(x, y)) -> $[([x] + [y])] hs: (_, Sum(x, y)) -> $[([x] + [y])]
hs: (_, Concat(x, y)) -> $[([x] ++ [y])]
hs: (Gets(_, xn), Gets(s, x)) -> v hs: (Gets(_, xn), Gets(s, x)) -> v
with v := <newname>"_fostr_get" with v := <newname>"_fostr_get"
@ -52,6 +58,10 @@ rules
hs_gets: (s, xn, x ) -> $[[s] [<hs_getOp>xn] [x]] hs_gets: (s, xn, x ) -> $[[s] [<hs_getOp>xn] [x]]
hs_getOp = get-type; (?STRING() < !"`getsStr`" + !"`gets`") hs_getOp = get-type; (?STRING() < !"`getsStr`" + !"`gets`")
hs: (_, Emits(s)) -> v
with v := <newname>"_fostr_emitted"
; <add-preactions>[$[[v] <- emit [s]]]
hs: (_, Terminate(x)) -> $[[x];;] hs: (_, Terminate(x)) -> $[[x];;]
hs: (_, Sequence(l)) -> <last>l hs: (_, Sequence(l)) -> <last>l
/* One drawback of using paramorphism is we have to handle lists /* One drawback of using paramorphism is we have to handle lists

View File

@ -3,24 +3,41 @@ imports libstrategolib signatures/- util
rules rules
js: TopLevel(x) -> $[// Fostr preamble js: TopLevel(x) -> $[// Fostr preamble
const _fostr_readline = require('readline');
const _fostr_events = require('events');
const _fostr_rl = _fostr_readline.createInterface({input: process.stdin});
const Stdio = { const Stdio = {
gets: v => { process.stdout.write(String(v)); return Stdio; }, gets: v => { process.stdout.write(String(v)); return Stdio; },
emit: async () => {
const [line] = await _fostr_events.once(_fostr_rl, 'line');
return line + "\n"; }
} }
function to(data, strm) { function to(data, strm) {
strm.gets(data); strm.gets(data);
return data; return data;
} }
const _fostr_body = async () => {
// End of preamble // End of preamble
[x]] [x]
// Fostr coda
_fostr_rl.close()
}
_fostr_body();
]
with line := "[line]"
js: Stream() -> $[Stdio] js: Stream() -> $[Stdio]
js: Int(x) -> x js: Int(x) -> x
js: LitString(x) -> <javaLitString>x js: LitString(x) -> <javaLitString>x
js: EscString(x) -> x js: EscString(x) -> x
js: Sum(x,y) -> $[[x] + [y]] js: Sum(x, y) -> $[[x] + [y]]
js: Concat(x, y) -> $[[x] + [y]]
js: Gets(x, y) -> $[[x].gets([y])] js: Gets(x, y) -> $[[x].gets([y])]
js: To(x, y) -> $[to([x],[y])] js: To(x, y) -> $[to([x],[y])]
js: Emits(x) -> $[(await [x].emit())]
js: Terminate(x) -> x js: Terminate(x) -> x
js: Sequence(l) -> <join(|";\n")>l js: Sequence(l) -> <join(|";\n")>l

66
trans/ocaml.str Normal file
View File

@ -0,0 +1,66 @@
module ocaml
imports libstrategolib signatures/- util signature/TYPE analysis
/* Note will use bottomup-para to traverse the full AST so that
we have access to the original expression (and can get the
Statix-associated type when we need to).
This means that every one of our local rules must take a pair
of an original term and a term with every child replaced by
its generated code.
*/
rules
ml: (_, TopLevel(x)) -> $[(* fostr preamble *)
type stream = { getS: string -> stream; emitS: unit -> string }
let rec stdio = {
getS = (fun s -> print_string s; stdio);
emitS = (fun () -> (read_line ()) ^ "\n");
};;
(* End of preamble *)
[x]]
ml: (_, Stream()) -> $[stdio]
ml: (_, Int(x)) -> x
ml: (_, LitString(x)) -> $[{|[<un-single-quote>x]|}]
ml: (_, EscString(x)) -> x
ml: (_, Sum(x, y)) -> $[[x] + [y]]
ml: (_, Concat(x, y)) -> $[[x] ^ [y]]
ml: (Gets(_,yn), Gets(x, y))
-> $[([x]).getS ([<ml_str>(yn,y)])]
ml: (To(xn,_), To(x, y))
-> $[let _fto = ([x]) in (ignore (([y]).getS ([<ml_str>(xn,"_fto")])); _fto)]
ml: (_, Emits(s)) -> $[[s].emitS ()]
ml: (_, Terminate(x)) -> x
ml: (_, Sequence(l)) -> <ml_seq>l
ml_seq: [x] -> x
ml_seq: [x | xs ] -> $[ignore ([x]);
[<ml_seq>xs]]
/* One drawback of using paramorphism is we have to handle lists
explicitly:
*/
ml: (_, []) -> []
ml: (_, [x | xs]) -> [x | xs]
/* Another drawback of using paramorphism is at the very leaves we have
to undouble the tuple:
*/
ml: (x, x) -> x where <is-string>x
ml_str: (node, code) -> $[[<ml_string_cast>node]([code])]
strategies
ml_string_cast = get-type; (?INT() < !"string_of_int" + !"")
ocaml = bottomup-para(try(ml))
// Interface ocaml code generation with editor services and file system
to-ocaml: (selected, _, _, path, project-path) -> (filename, result)
with filename := <guarantee-extension(|"ml")> path
; result := <ocaml> selected

View File

@ -8,6 +8,8 @@ rules
def gets(self, v): def gets(self, v):
print(v, file=sys.stdout, end='') print(v, file=sys.stdout, end='')
return self return self
def emit(self):
return input() + "\n" # Python inconsistently strips when using input
def to(data,strm): def to(data,strm):
strm.gets(data) strm.gets(data)
return data return data
@ -21,8 +23,10 @@ rules
py: LitString(x) -> $[r[x]] py: LitString(x) -> $[r[x]]
py: EscString(x) -> x py: EscString(x) -> x
py: Sum(x,y) -> $[[x] + [y]] py: Sum(x,y) -> $[[x] + [y]]
py: Concat(x,y) -> $[[x] + [y]]
py: Gets(x, y) -> $[[x].gets([y])] py: Gets(x, y) -> $[[x].gets([y])]
py: To(x, y) -> $[to([x],[y])] py: To(x, y) -> $[to([x],[y])]
py: Emits(x) -> $[[x].emit()]
py: Terminate(x) -> $[[x];] py: Terminate(x) -> $[[x];]
py: Sequence(l) -> <join(|"\n")>l py: Sequence(l) -> <join(|"\n")>l

View File

@ -222,6 +222,13 @@ This pattern lets us specify error messages.
type_Ex(e2) == STREAM() | error $[Items may only be sent to Streams.]@e2. type_Ex(e2) == STREAM() | error $[Items may only be sent to Streams.]@e2.
/* **/ /* **/
ty_Ex(Concat(e1, e2)) = STRING() :-
type_Ex(e1) == STRING() | error $[Expression [e1] not String in concat.]@e1,
type_Ex(e2) == STRING() | error $[Expression [e2] not String in concat.]@e2.
ty_Ex(Emits(e)) = STRING() :- // At the moment, only stream is stdio
type_Ex(e) == STREAM() | error $[Only Streams may emit items.]@e.
/** md /** md
``` ```