*拍打 [编程语言] 的屋顶* 这个坏小子可以将这么多 [语法糖] 纳入其中
An attempt to give myself a new Pareto-optimal choice for quick-and-dirty scripts, particularly when I'm not on a dev computer, and to practice writing a more realistic programming language instead of the overengineered stack-based nonsense I spend too much time on. (Crafting Interpreters is such a good book, I have no excuses.)
You can try Noulith online (via wasm)!
Immutable data structures (but not variables) means you can write matrix = [[0] ** 10] ** 10; matrix[1][2] = 3 and not worry about it, instead of the [[0] * 10 for _ in range(10)] you always have to do in Python. You can also freely use things as keys in dictionaries. But, thanks to mutate-or-copy-on-write shenanigans behind the scenes (powered by Rust's overpowered reference-counting pointers), you don't have to sacrifice the performance you'd get from mutating lists. (There are almost certainly space leaks from cavalier use of Rc but shhhhh.)
Everything is an infix operator; nearly everything can be partially applied. If you thought Scala had a lot of syntax sugar, wait till you see what we've got.
noulith> 1 to 10 filter even map (3*)
[6, 12, 18, 24, 30]
Ever wanted to write x max= y while searching for some maximum value in some complicated loop? You can do that here. You can do it with literally any function.
You know how Python has this edge case where you can write things like {1} and {1, 2} to get sets, but {} is a dictionary because dictionaries came first? We don't have that problem because we don't distinguish sets and dictionaries.
Operator precedence is customizable and resolved at runtime.
noulith> f := \-> 2 + 5 * 3
noulith> f()
17
noulith> swap +, *
noulith> f() # (2 times 5) plus 3
13
noulith> swap +::precedence, *::precedence
noulith> f() # 2 times (5 plus 3)
16
noulith> swap +, *
noulith> f() # (2 plus 5) times 3
21
Imagine all the operator parsing code you won't need to write. When you need like arbitrarily many levels of operator precedence, and are happy to eval inputs.
It's a standard Rust project, so, in brief:
cd to itcargo run --release --features cli,request,cryptoThis will drop you into a REPL, or you can pass a filename to run it. If you just want to build an executable so you can alias it or add it to $PATH, just run cargo build --release --features cli,request,crypto and look inside target/release.
None of the command-line options to cargo run or cargo build are required; they just give you better run-time performance and features for a slower compile time and larger binary size. (Without --release, stack frames are so large that one of the tests overflows the stack...)
:=. (I never would have considered this on my own, but then I read the Crafting Interpreters design note and was just totally convinced.)++. String concatenation is $. Maybe? Not sure yet.switch, try, apparently.if (condition) body else body, for (thing) body (not the modern if cond { body }). The if ... else is the ternary expression.[a, b, c]. Dictionaries are curly braces: {a, b, c}. We don't bother with a separate set type, but dictionaries often behave quite like their sets of keys.for (x x + y.Somewhat imperative:
for (x if (x % f == 0) s else "") join "" or x)
NOTE: I will probably keep changing the language and may not keep all this totally up to date.
Numbers, arithmetic operators, and comparisons mostly work as you'd expect, including C-style bitwise operators, except that:
^ is exponentiation. Instead, ~ as a binary operator is xor (but can still be unary as bitwise complement). Or you can just use xor./ does perfect rational division like in Common Lisp or something. % does C-style signed modulo. // does integer division rounding down, and %% does the paired modulo (roughly).Tighter ^ >
* / % &
+ - ~
|
Looser == != =
We support arbitrary radixes up to 36 with syntax 36r1000 == 36^3, plus specifically the slightly weird base-64 64rBAAA == 64^3 (because in base-64 A is 0, B is 1, etc.)
Like in Python and mathematics, comparison operators can be chained like 1 and its reverse >=, and =, operators ending in = will be parsed as the operator followed by an =, so in general operators cannot end with =.
Almost all builtin functions' precedences are determined by this Scala-inspired rule: Look up each character in the function's name in this table, then take the loosest precedence of any individual character. But note that this isn't a rule in the syntax, it's just a strategy I decided to follow when selecting builtin functions' precedences. For example, +, ++, .+, and +. all have the same precedence. As of time of writing, the only exceptions to this rule are >, which have precedence like ^.
Tighter . (every other symbol, mainly @ which I haven't allocated yet)
!?
^
*/%&
+-~
|
$
=<>
Looser (alphanumerics)
. is not special syntax, it's actually just an operator that does tightly-binding reverse function application! a.b = b(a). then is loosely-binding reverse function application.
! is syntax that's spiritually sort of like what Haskell's $ lets you write. It's as tight as an opening parenthesis on its left, but performs a function call that lets you can omit the closing one up to the next semicolon or so. f! a, b is f(a, b).
So, these three expressions are equivalent (assuming the built-in . hasn't been reassigned or shadowed):
print foo
print(foo)
foo.print
As are these:
max(foo, bar)
foo max bar
max! foo, bar
_ is special; assigning to it discards (but type checks still happen; see below). Some expressions produce Scala-style anonymous functions, e.g. `1 (print x; a)
x .= f
This allows us to not have to keep an extra copy of the LHS variable in common cases where we "modify" it, so code like `x append= y` is actually efficient (see discussion of immutability below).
The weird keyword `every` lets you assign to or operate on multiple variables or elements of a slice at once. This initializes three variables to `1`. This doesn't work with operator-assignments, though it might in the future.
every a, b, c := 1
After this, `x == [0, 0, 1, 1, 0]`.
x := [0] ** 5; every x[2:4] = 1
Important note about assignment: **All data structures are immutable.** When we mutate indexes, we make a fresh copy to mutate if anything else points to the same data structure. So for example, after
x := [1, 2, 3]; y := x; x[0] = 4
…
x := [1, 2, 3, 4, 5]; y := pop x; z := remove x[0]
`y` will be `5`, `z` will be `1`, and `x` will be `[2, 3, 4]`. There's no way to implement `pop` as a function yourself; the best you could do is take a list and separately return the last element and everything before it.
You can implement your own "mutable data cells" easily (?) with a closure:
make_cell := \init -> (x := init; [\ -> x, \y -> (x = y)]) get_a, set_a := make_cell(0)
### Control Flow
As above: statements must be separated by semicolons.
Everything is an expression, so the "ternary expression" and if/else statement are one and the same: `if (a) b else c`. Loops: `for (var b
case 2 -> d
Run-time type checking does some work here:
switch (x)
case _: int -> print("it's an int")
case _ -> print("not sure")
Stupid complicated runtime types with satisfying:
switch (x)
case _: satisfying! 1 print("it's between 1 and 9")
case _ -> print("not sure")
Don't do weird things in the argument to satisfying, it's illegal. (Also actually you can just write this because the comparison operators ` print("it's between 1 and 9")
case _ -> print("not sure")
…
struct Foo (bar, baz = "default");
…
x := iterate! 0, \t -> x const t x[0] = 0
…
Other goodies: id, const (returns its second argument!), flip. Some Haskell-Arrow-esque operators exist: &&&, ***, >>>, <<<. The first two are n-ary like zip.
print: space-separated newline-terminated
echo: space-separated
write: just concatenated
debug: debug
input: read line
read: read all
read_file read_file? read_file_bytes read_file_bytes?
write_file append_file These take the file as the second argument to better support partial application, but I feel like I'll regret this soon.
(current implementation completely disrespects cross-OS unicode things) path_join path_parent
time now
If compiled with request:
request("https://httpbin.org/", {"method": "POST", "headers": {"Foo": "Bar"}})weird things that are faster to evaluate than always making/following chains of environments, looking up variable names in maps, etc. in theory the hope is that we can automatically translate code to use these things to optimize them, in practice it's a ton of work lol.
__internal_frame expr: record the stack's length, execute the body, then truncate the stack back to the same length (if the stack is too short you're on your own)__internal_push expr: push something onto the stack__internal_pop: pop something from the stack (and return it)__internal_peek integer-const: get or assign to some position, 0-indexed from the top of the stack__internal_for (expr) body:__internal_call integer-const expr: pop the top n elements of the stack, then call the expression with those as arguments (bottom to top)__internal_lambda [captures] n body: makes an internal lambda that, when called, doesn't enter a new environment; but records the stack length, pushes any captures followed by the arguments on, then executes and can return a value as usual, restoring the stack length before returning. n is the number of arguments accepted.On the wasm version, lists and dictionaries can be pretty-printed and there's a HtmlTag(html_tag_name, html_tag_children, html_tag_attributes) struct that gets rendered out dynamically as HTML if your code evaluates to it. There's a fa
暂无开放 Issues,或尚未同步最近议题。