#!/usr/bin/env bash # -*- mode: sh; sh-basic-offset: 2 -*- proc usage { echo "$0 [OPTION] FILE..." echo echo "Options:" echo " -e|--eval STR Evaluate STR" echo " -l|--load FILE Load and evaluate FILE" echo " -r|--repl Start a REPL" exit 2 } if [[ -z "$BASH_VERSION" ]] || sh-expr '${BASH_VERSINFO[0]} < 4' { echo "bash >= 4.0 required" > !2 exit 1 } setglobal DEFAULT_IFS = $IFS # function return and error slots ############################################## setglobal r = ''"" setglobal e = ''"" proc error { [[ -z "$e" ]] && setglobal e = $1 || setglobal e = ""$1\n$e"" setglobal r = $NIL return 1 } # pushback reader ############################################################## setglobal pb_max = '100' setglobal pb_newline = $[printf '\034] setglobal pb_star = $[printf '\035] setglobal pb_get = '"^$'" setglobal pb_unget = '"^$'" setglobal history_flag = '1' proc readline { local IFS=$'\n\b' prompt="> " line set -f read -e -r -p $prompt line || exit 0 setglobal pb_get = ""$(pb_get:0:$((${#pb_get}-1)))$(line)$(pb_newline)$"" set +f unset IFS if [[ "$line" =~ ^[[:space:]]*- ]] { echo "warning: lines starting with - aren't stored in history" > !2 } elif [[ -n "$history_flag" ]] { history -s $line } } proc getc { local ch if sh-expr '${#pb_get} == 2' { readline getc } else { setglobal ch = $(pb_get:1:1) setglobal pb_get = ""^$(pb_get:2)"" if sh-expr 'pb_max > 0' { setglobal pb_unget = $(pb_unget:0:$((${#pb_unget}-1))) setglobal pb_unget = ""^$(ch)$(pb_unget:1:$((pb_max-1)))$"" } else { setglobal pb_unget = ""^$(ch)$(pb_unget:1)"" } setglobal r = $ch } } proc ungetc { if [[ "$pb_unget" == "^$" ]] { echo "ungetc: nothing more to unget, \$pb_max=$pb_max" > !2 && return 1 } else { setglobal pb_get = ""^$(pb_unget:1:1)$(pb_get:1)"" setglobal pb_unget = ""^$(pb_unget:2)"" } } proc has_shebangP { [[ "$(head -1 $1)" =~ ^#! && "$(head -1 $1)" =~ gherkin ]]; } proc strmap_file { local f="$1" contents if has_shebangP $f { setglobal contents = $[tail -n+2 $f | sed -e 's/^[[:space:]]*//' | tr -s '\n' $pb_newline] } else { setglobal contents = $[cat $f | sed -e 's/^[[:space:]]*//' | tr -s '\n' $pb_newline] } setglobal mapped_file = ""(do $contents nil)"" setglobal mapped_file_ptr = '0' } proc strmap_getc { setglobal r = $(mapped_file:$((mapped_file_ptr++)):1) } proc strmap_ungetc { let --mapped_file_ptr } setglobal _getc = 'getc' setglobal _ungetc = 'ungetc' # memory layout & gc ########################################################### setglobal cons_ptr = '0' setglobal symbol_ptr = '0' setglobal protected_ptr = '0' setglobal gensym_counter = '0' setglobal array_cntr = '0' declare -A interned_strings declare -A car declare -A cdr declare -A environments declare -A recur_frames declare -A recur_fns declare -A marks declare -A global_bindings declare -a symbols declare -a protected declare -a mark_acc setglobal heap_increment = '1500' setglobal cons_limit = $heap_increment setglobal symbol_limit = $heap_increment setglobal tag_marker = $[printf '\036] setglobal atag = ""$(tag_marker)003"" declare -A type_tags=([000]=integer [001]=symbol [002]=cons [003]=vector [004]=keyword) proc type { if [[ "${1:0:1}" == "$tag_marker" ]] { setglobal r = $(type_tags[${1:1:3}]) } else { setglobal r = 'string' } } proc strip_tag { setglobal r = $(1:4); } proc typeP { local obj="$1" tag="$2" type $obj && [[ $r == "$tag" ]] } proc make_integer { setglobal r = ""$(tag_marker)000$(1)""; } proc make_keyword { setglobal r = ""$(tag_marker)004$(1:1)""; } proc intern_symbol { if [[ -n "${interned_strings[$1]}" ]] { setglobal r = $(interned_strings[$1]) } else { setglobal symbol_ptr = "$shExpr('symbol_ptr + 1')" compat array-assign interned_strings '"$1"' ""$(tag_marker)001$(symbol_ptr)"" compat array-assign symbols '"$symbol_ptr"' $1 setglobal r = ""$(tag_marker)001$(symbol_ptr)"" } } proc defprim { intern_symbol $1 && setglobal sym_ptr = $r intern_symbol $[printf '#' $1] && setglobal prim_ptr = $r compat array-assign global_bindings '"$sym_ptr"' $prim_ptr setglobal r = $prim_ptr } proc cons { local the_car="$1" the_cdr="$2" mark $the_car mark $the_cdr { unset marks["$(tag_marker)002$shExpr('cons_ptr++')"] } if [[ $cons_ptr == $cons_limit ]] { gc } unset environments["$(tag_marker)002$(cons_ptr)"] unset recur_frames["$(tag_marker)002$(cons_ptr)"] unset recur_fns["$(tag_marker)002$(cons_ptr)"] compat array-assign car '"${tag_marker}002${cons_ptr}"' $the_car compat array-assign cdr '"${tag_marker}002${cons_ptr}"' $the_cdr setglobal r = ""$(tag_marker)002$(cons_ptr)"" setglobal cons_ptr = "$shExpr('cons_ptr + 1')" } proc gensym { setglobal gensym_counter = $shExpr('gensym_counter + 1') intern_symbol "G__$(gensym_counter)" } proc new_array { setglobal r = ""arr$shExpr('array_cntr++')"" declare -a $r setglobal r = ""$(atag)$(r)"" } proc vset { strip_tag $1 eval "$(r)[$(2)]=\"$(3)\"" setglobal r = $1 } proc vget { strip_tag $1 eval "r=\${$(r)[$(2)]}" } proc count_array { strip_tag $1 eval "r=\${#$(r)[@]}" } proc append { local i strip_tag $1 eval "i=\${#$(r)[@]}" eval "$(r)[$(i)]=\"$(2)\"" setglobal r = $1 } proc append_all { strip_tag $1 local a1="$r" strip_tag $2 local a2="$r" local len1 len2 eval "len1=\${#$(a1)[@]}" eval "len2=\${#$(a2)[@]}" local i=0 while sh-expr 'i < len2' { eval "$(a1)[(($(i) + $(len1)))]=\"\${$(a2)[$(i)]}\"" sh-expr 'i++' } setglobal r = $1 } proc prepend { local i len strip_tag $2 eval "len=\${#$(r)[@]}" while sh-expr 'len > 0' { eval "$(r)[$(len)]=\"\${$(r)[((len - 1))]}\"" sh-expr 'len--' } eval "$(r)[0]=\"$1\"" setglobal r = $2 } proc dup { new_array local aptr="$r" strip_tag $aptr local narr="$r" strip_tag $1 local len eval "len=\${#$(r)[@]}" local i=0 while sh-expr 'i < len' { eval "$(narr)[$(i)]=\"\${$(r)[$(i)]}\"" sh-expr 'i++' } setglobal r = $aptr } proc concat { dup $1 append_all $r $2 } proc vector { local v="$2" if [[ "$EMPTY" == "$v" || -z "$v" || "$NIL" == "$v" ]] { new_array setglobal v = $r } prepend $1 $v } proc protect { setglobal protected_ptr = "$shExpr('protected_ptr + 1')" compat array-assign protected '"$protected_ptr"' $1 } proc unprotect { setglobal protected_ptr = "$shExpr('protected_ptr - 1')"; } setglobal acc_count = '0' proc mark_seq { local object="$1" while typeP $object cons { compat array-assign marks '"$object"' '1' compat array-assign mark_acc 'acc_count++' $(car[$object]) setglobal object = $(cdr[$object]) } if typeP $object vector { count_array $object local i sz="$r" for ((i=0; i !2 setglobal IFS = $DEFAULT_IFS mark $current_env for k in [$(!environments[@])] { mark $(environments[$k]); } for k in [$(!protected[@])] { mark $(protected[$k]); } for k in [$(!stack[@])] { mark $(stack[$k]); } for k in [$(!global_bindings[@])] { mark $(global_bindings[$k]); } setglobal cons_ptr = '0' { unset marks["$(tag_marker)002$shExpr('cons_ptr++')"] } if [[ $cons_ptr == $cons_limit ]] { echo "expanding heap..." > !2 setglobal cons_limit = $shExpr('cons_limit + heap_increment') } } # reader ####################################################################### proc interpret_token { [[ "$1" =~ ^-?[[:digit:]]+$ ]] \ && setglobal r = 'integer' && return [[ "$1" =~ ^:([[:graph:]]|$pb_star)+$ ]] \ && setglobal r = 'keyword' && return [[ "$1" =~ ^([[:graph:]]|$pb_star)+$ ]] \ && setglobal r = 'symbol' && return return 1 } proc read_token { local token="" while $_getc { if [[ "$r" =~ ('('|')'|'['|']'|[[:space:]]|$pb_newline|,) ]] { $_ungetc && break } else { setglobal token = ""$(token)$(r/\*/${pb_star})"" } } test -z $token && return 1 if interpret_token $token { match $r { with symbol intern_symbol $token && return with integer make_integer $token && return with keyword make_keyword $token && return with * error "unknown token type: '$r'" } } else { error "unknown token: '$(token)'" } } proc skip_blanks { $_getc { $_getc; } $_ungetc } proc skip_comment { $_getc { $_getc; } } proc read_list { local ch read1 read2 if lisp_read { setglobal read1 = $r } else { $_getc setglobal r = $NIL return } $_getc && setglobal ch = $r match $ch { with "." lisp_read && setglobal read2 = $r skip_blanks $_getc cons $read1 $read2 with ")" cons $read1 $NIL with * $_ungetc read_list cons $read1 $r } } proc read_vector { local ch read1 if lisp_read { setglobal read1 = $r } else { getc setglobal r = $EMPTY return } skip_blanks getc if [[ "$r" == "]" ]] { vector $read1 $EMPTY } else { ungetc skip_blanks read_vector vector $read1 $r } } proc read_string { local s="" while true { $_getc if [[ "$r" == "\\" ]] { $_getc if [[ "$r" == '"' ]] { setglobal s = ""$(s)$(r)"" } else { setglobal s = ""$(s)\\$(r)"" } } elif [[ "$r" == '"' ]] { break } else { setglobal s = ""$(s)$(r)"" } } setglobal r = $[echo $s | tr $pb_star '*] } proc lisp_read { local ch read1 read2 read3 read4 skip_blanks; $_getc; setglobal ch = $r match $ch { with "\"" read_string with "(" read_list with "[" read_vector with "'" lisp_read && setglobal read1 = $r cons $read1 $NIL && setglobal read2 = $r cons $QUOTE $read2 with ";" skip_comment lisp_read with * $_ungetc read_token } } proc string_list { local c="$1" ret shift if [[ "$1" == "" ]] { cons $c $NIL && setglobal ret = $r } else { string_list $ifsjoin(Argv) cons $c $r && setglobal ret = $r } setglobal r = $ret } # printer ###################################################################### setglobal printing = '' proc escape_str { local i c setglobal r = ''"" for ((i=0; i < ${#1}; i++)); do c="${1:$i:1}" case "$c" in \") r="${r}\\\"" ;; \\) r="${r}\\\\" ;; *) r="${r}${c}" esac done } proc str_arr { local ret="[" count_array $1 local len=$r if sh-expr ' 0 != len ' { vget $1 0 str $r setglobal ret = ""$(ret)$(r)"" for ((i=1 ; i < $len ; i++)); do vget $1 $i str "$r" ret="${ret} ${r}" done } setglobal r = ""$(ret)]"" } proc str_list { local lst="$1" local ret if [[ "${car[$lst]}" == $FN ]] { strip_tag $lst && printf -v r '#' $r } else { setglobal ret = '"('" str $(car[$lst]) setglobal ret = ""$(ret)$(r)"" setglobal lst = $(cdr[$lst]) while typeP $lst cons { str $(car[$lst]) setglobal ret = ""$(ret) $(r)"" setglobal lst = $(cdr[$lst]) } if [[ "$lst" != $NIL ]] { str $lst setglobal ret = ""$(ret) . $(r)"" } setglobal r = ""$(ret))"" } } proc str { type $1 match $r { with integer strip_tag $1 && printf -v r '%d' $r with cons str_list $1 with vector str_arr $1 with symbol strip_tag $1 && printf -v r '%s' $[echo $(symbols[$r]) | tr $pb_star "*] with keyword strip_tag $1 && printf -v r ':%s' $r with * if [[ -n $printing ]] { escape_str $1 printf -v r '"%s"' $r } else { printf -v r '%s' $1 } } } proc prn { setglobal printing = '1' str $1 setglobal printing = '' printf '%s' $r && echo } # environment & control ######################################################## setglobal frame_ptr = '0' setglobal stack_ptr = '0' declare -a stack intern_symbol '&' && setglobal AMP = $r intern_symbol 'nil' && setglobal NIL = $r intern_symbol 't' && setglobal T = $r compat array-assign global_bindings '$NIL' $NIL compat array-assign global_bindings '$T' $T compat array-assign car '$NIL' $NIL compat array-assign cdr '$NIL' $NIL new_array && setglobal EMPTY = $r setglobal current_env = $NIL intern_symbol 'quote' && setglobal QUOTE = $r intern_symbol 'fn' && setglobal FN = $r intern_symbol 'if' && setglobal IF = $r intern_symbol 'set!' && setglobal SET_BANG = $r intern_symbol 'def' && setglobal DEF = $r intern_symbol 'do' && setglobal DO = $r intern_symbol 'recur' && setglobal RECUR = $r intern_symbol 'binding' && setglobal BINDING = $r declare -A specials compat array-assign specials '$QUOTE' '1' compat array-assign specials '$FN' '1' compat array-assign specials '$IF' '1' compat array-assign specials '$SET_BANG' '1' compat array-assign specials '$DEF' '1' compat array-assign specials '$DO' '1' compat array-assign specials '$RECUR' '1' compat array-assign specials '$BINDING' '1' defprim 'eq?' && setglobal EQ = $r defprim 'nil?' && setglobal NILP = $r defprim 'car' && setglobal CAR = $r defprim 'cdr' && setglobal CDR = $r defprim 'cons' && setglobal CONS = $r defprim 'list' && setglobal LIST = $r defprim 'vector' && setglobal VECTOR = $r defprim 'keyword' && setglobal KEYWORD = $r defprim 'eval' && setglobal EVAL = $r defprim 'apply' && setglobal APPLY = $r defprim 'read' && setglobal READ = $r defprim '+' && setglobal ADD = $r defprim '-' && setglobal SUB = $r defprim $pb_star && setglobal MUL = $r defprim '/' && setglobal DIV = $r defprim 'mod' && setglobal MOD = $r defprim '<' && setglobal LT = $r defprim '>' && setglobal GT = $r defprim 'cons?' && setglobal CONSP = $r defprim 'symbol?' && setglobal SYMBOLP = $r defprim 'number?' && setglobal NUMBERP = $r defprim 'string?' && setglobal STRINGP = $r defprim 'fn?' && setglobal FNP = $r defprim 'gensym' && setglobal GENSYM = $r defprim 'random' && setglobal RAND = $r defprim 'exit' && setglobal EXIT = $r defprim 'println' && setglobal PRINTLN = $r defprim 'sh' && setglobal SH = $r defprim 'sh!' && setglobal SH_BANG = $r defprim 'load-file' && setglobal LOAD_FILE = $r defprim 'gc' && setglobal GC = $r defprim 'error' && setglobal ERROR = $r defprim 'type' && setglobal TYPE = $r defprim 'str' && setglobal STR = $r defprim 'split' && setglobal SPLIT = $r defprim 'getenv' && setglobal GETENV = $r proc eval_args { local args="$1" type $args if [[ "$r" == cons ]] { { lisp_eval $(car[$args]) compat array-assign stack '$((stack_ptr++))' $r setglobal args = $(cdr[$args]) } } elif [[ "$r" == vector ]] { count_array $args local i len="$r" for ((i=0; i y' && setglobal r = $T || setglobal r = $NIL } with $CONSP typeP $arg0 cons && setglobal r = $T with $SYMBOLP typeP $arg0 symbol && setglobal r = $T || setglobal r = $NIL with $NUMBERP typeP $arg0 integer && setglobal r = $T || setglobal r = $NIL with $STRINGP typeP $arg0 string && setglobal r = $T || setglobal r = $NIL with $FNP typeP $arg0 cons && [[ "${car[$arg0]}" == $FN ]] && setglobal r = $T with $GC gc && setglobal r = $NIL with $GENSYM gensym with $ADD if check_numbers $arg0 $arg1 { strip_tag $arg0 && local x="$r" strip_tag $arg1 && local y="$r" make_integer $shExpr('x + y') } with $SUB if check_numbers $arg0 $arg1 { strip_tag $arg0 && local x="$r" strip_tag $arg1 && local y="$r" make_integer $shExpr('x - y') } with $APPLY local old_frame_ptr=$frame_ptr setglobal frame_ptr = $stack_ptr type $arg1 match $r { with cons while typeP $arg1 cons { compat array-assign stack '$((stack_ptr++))' $(car[$arg1]) setglobal arg1 = $(cdr[$arg1]) } [[ $arg1 != $NIL ]] && error "Bad argument to apply: not a proper list" with vector count_array $arg1 local len="$r" for ((i=0; i !2 prn $arg0 > !2 with $TYPE if [[ "$arg0" == $NIL ]] { setglobal r = $NIL } else { type $arg0 if [[ "$r" == cons ]] && [[ "${car[$arg0]}" == $FN ]] { intern_symbol "function" } else { intern_symbol $r } } with $MUL if check_numbers $arg0 $arg1 { strip_tag $arg0 && local x="$r" strip_tag $arg1 && local y="$r" make_integer $shExpr('x * y') } with $DIV local x y if check_numbers $arg0 $arg1 { strip_tag $arg0 && setglobal x = $r strip_tag $arg1 && setglobal y = $r make_integer $shExpr('x / y') } with $RAND if check_numbers $arg0 { strip_tag $arg0 make_integer "$shExpr('RANDOM % r')" } with $PRINTLN listify_args && local to_print="$r { type $(car[$to_print]) match $r { with string echo -e $(car[$to_print]) with * prn $(car[$to_print]) } setglobal to_print = $(cdr[$to_print]) } setglobal r = $NIL with $SH local ret eval "ret=\$($(arg0))" setglobal IFS = '$'\n'' string_list $[for i in [$ret] { echo $i; }] setglobal IFS = $DEFAULT_IFS with $SH_BANG eval $(arg0) [[ $? == 0 ]] && setglobal r = $T || setglobal r = $NIL with $LOAD_FILE local f if [[ -r ${arg0} ]] { setglobal f = $(arg0) } elif [[ -r "${arg0}.gk" ]] { setglobal f = ""$(arg0).gk"" } if [[ "$f" != "" ]] { eval_file $f } else { echo "File not found: $(arg0)" > !2 setglobal r = $NIL } with $EXIT strip_tag $arg0 exit "$r" with * strip_tag $1 && error "unknown primitive function type: $(symbols[$r])" return 1 } } proc apply { if [[ "${car[$1]}" == "$FN" ]] { apply_user $1 } else { apply_primitive $1 } } proc add_bindings { type $1 if [[ $r == cons ]] { local pairs="$1" val { lisp_eval $(car[${cdr[$pairs]}]) && setglobal val = $r if [[ -n "$e" ]] { return 1; } acons $(car[$pairs]) $val $current_env && setglobal current_env = $r setglobal pairs = $(cdr[${cdr[$pairs]}]) } if [[ "$pairs" != $NIL ]] { error "Bad bindings. Must be an even number of binding forms." return 1 } } elif [[ "$r" == vector ]] { count_array $1 local i v len="$r" if sh-expr ' len % 2 == 0 ' { for (( i=0; i !2 protect $r lisp_eval $r update_history [[ -n "$e" ]] && printf "eval error: $e\n" > !2 prn $r [[ -n "$e" ]] && printf "print error: $e\n" > !2 unprotect } } # start ######################################################################## proc eval_string { local str="$1" lisp_read <<<"(do $str)" protect $r lisp_eval $r unprotect } # Start REPL if no arguments test -z "$ifsjoin(Argv)" && repl setglobal ARGV = "$ifsjoin(Argv)" # Process parameters while test "$ifsjoin(Argv)" { setglobal param = $1; shift; setglobal OPTARG = $1 match $param { with -e|--eval eval_string $OPTARG; shift [[ $r != $NIL ]] && prn $r with -l|--load eval_file $OPTARG; shift [[ $r != $NIL ]] && prn $r with -t|--test with -r|--repl repl with -* usage with * [[ -f "$param" ]] && has_shebangP $param && eval_file $param [[ $r != $NIL ]] && prn $r } }