A low-level, postfix, functional programming language that compiles to WebAssembly
A very low-level functional programming language designed to compile to WebAssembly in the browser. The language actually bears very little resemblance to Haskell despite the name.
The all examples can be run in an interactive shell like below. Alternatively you can install the more stable npm package). For better examples check out the recently edited files in the planning/*
folder. Also note the standard library in /std/*
.
$ git clone https://github.com/dvtate/postfix-haskell
$ cd postfix-haskell
$ npm run build
$ npm install --global
$ phc shell
> "lang" require use
> 1 2 + :data
:data - 3n
The compiler now has a main program that lets you use it through the command line. This can be done by first doing npm run build
and then either run it locally as node dist/index.js
or install it to your machine via npm install --global
so that you can use it via phc
.
[postfix-haskell]$ npm run build
[postfix-haskell]$ sudo npm install --global
[postfix-haskell]$ phc --help
phc <command> [args]
Commands:
phc shell [options] run interactive shell [default]
phc file <name> [options] compile a file to WAT
Options:
--version Show version number [boolean]
-v, --verbose include verbose output [boolean] [default: false]
--help Show help [boolean]
-l, --lex debug lexer tokens [boolean] [default: false]
The shell is probably the best way to learn the language, allowing you to run short bits of code and test expected compiler behavior. In addition to normal code there exist some compiler macros that make debugging easier you can find these in lib/debug_macros.ts
, for example:
[postfix-haskell]$ npm start
> "lang" require use
> 1 2 + :data
:data - 3n
> 1 2 + :type
:type - {
syntaxType: 'Data',
datatype: PrimitiveType { token: undefined, name: 'i32' }
}
> ( I32 ) (: 1 + ) "incr" export :compile
:compile - (module
(func (;0;) (param i32) (result i32)
local.get 0
i32.const 1
i32.add)
(export "incr" (func 0))
(memory (;0;) 1)
(export "memory" (memory 0))
(data (;0;) (i32.const 0) "")
(type (;0;) (func (param i32) (result i32))))
For compiling a file to WASM Text format with expectation of errors. Once you know it compiles you can use tools/optimized.sh
to get an optimized binary
phc file <name> [options]
compile a file to WAT
Positionals:
name name of the file to open [string] [required]
Options:
--version Show version number [boolean]
-v, --verbose include verbose output [boolean] [default: false]
--help Show help [boolean]
-t, --track-time track time spent compiling [boolean] [default: true]
--fast skip validation and pretty-print steps
[boolean] [default: false]
--folding use folding/s-expr WAT syntax [boolean] [default: false]
-O, --optimize pass compiled output through binaryen optimizer
[default: false]
You can embed the language in JavaScript or TypeScript. See a demo in planning/inline.ts
.
=
operator1 2 +
)
+
and &&
.
"/[path to this repo]/postfix-haskell/std/lang.phs" require use
$name
) can be used to store any type of value@
: this is the same as unecaping the identifier, it will invoke values~
: this will unescape the the value but will not invoke it# equiv to `let a = 4 * (1 + 2)`
1 2 + 4 * $a =
# Prints `:data - 12` at compile time
a :data
# Macro that returns the next number
# Notice we specified optional type annotations
((I32)(I32): 1 + ) $incr =
# :data - 6
5 incr :data
5 1 + :data
# (a,b)->(b,a)
(: ( $a $b ) = b a ) $swap =
# (a)->()
(: $_ = ) $pop =
# ()->(a,b)
# Notice the optional type annotations
(()(F64 F64): 1.0 2.0 ) $nums =
nums / :data # 0.5
nums swap / :data # 2
nums pop :data # 1
Notice that if we want to get the macro stored in a variable we cannot simply unescape it, and must use the ~
operator.
(: $op = 1 2 op ) $apply_operator =
(: + 2 *) $add_and_double =
# This is valid
$add_and_double ~ apply_operator :data # 6
# This is wrong and will not compile
add_and_double apply_operator :data
{ condition } { action } $identifier fun
# Use bigger of two values
# Here we're checking a runtime condition
(: true ) (: pop ) $max fun
(: < ) (: ( $a $b ) = b ) $max fun
# Here we're checking a compile-time condition
((F32): 1 ) (: "f32.max" asm ) $max fun
# This does basically the same as before but for F64
(: type F64 == ) (: "f64.max" asm ) $max fun
# This takes the F64 branch
1.0 30.1 max :data # 30.1
# This takes the (: < ) branch
# Condition: 30 gets promoted to 30.0 causing it to take the branch
# Action: the other 30 remains on the stack as the result
1.2 30 max :data # 30
namespace
keyword.
between the namespace identifier and the identifier to access.global
namespace is available at all scopesuse
keyword applies namespaces identifiers to current scoperequire
keyword which gives a namespaceexport
keyword# Import basic language features such as `+` and `-` among other things
# And apply it to the current scope
"./std/lang.phs" require use
# Create a namespace 'ns'
(:
5 $five =
(: + ) $add =
) namespace $ns =
10 $five =
( global.I32 ) (:
# Get 'five' from the namespace
ns.five
# Invoke 'add' from the namespace
ns.add
five -
) "demo" export
TODO