git.m455.casa

perl-lisp

clone url: git://git.m455.casa/perl-lisp


lip.pl

1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 # i used to need to disable the deep recursion warnings before i learned about
7 # the `@_ = whatever; goto &subroutine` trick
8 # i still probably need it but whatever lol. can't figure some alternatives out
9 # no warnings 'recursion';
10
11 # utf8 it all ...at least that's what i hope i'm doing
12 use utf8;
13 use open ":std", ":encoding(UTF-8)";
14 use Scalar::Util qw(looks_like_number);
15
16 # pretty printing
17 use Data::Dumper;
18 $Data::Dumper::Terse = 1; # don't prefix things with stuff like $VAR->whatever
19 $Data::Dumper::Indent = 2; # style of output/indent/indexes/spaces
20 $Data::Dumper::Useqq = 1; # use double quotes for strings
21 $Data::Dumper::Deepcopy = 1; # don't show $VAR->[1] for deeply nested shit
22 sub pp { print(Dumper(@_)); }
23
24 use constant {
25 AST_NUMBER => 'number',
26 AST_STRING => 'string',
27 AST_KEYWORD => 'keyword',
28 AST_SYMBOL => 'symbol',
29 OK => ':ok'
30 };
31
32 my $environment = {
33 'str' => sub {
34 return lisp_string($_[0]);
35 },
36 'file->str' => sub {
37 return read_file($_[0]);
38 },
39 'function?' => sub {
40 return (ref($_[0]) eq 'HASH');
41 },
42 'list?' => sub {
43 return (ref($_[0]) eq 'ARRAY');
44 },
45 'number?' => sub { return looks_like_number($_[0]); },
46 'string?' => sub {
47 return (
48 not looks_like_number($_[0]) and
49 ref($_[0]) ne 'ARRAY'
50 );
51 },
52 'keyword?' => sub {
53 return (
54 not looks_like_number($_[0]) and
55 ref($_[0]) ne 'ARRAY' and
56 substr($_[0], 0, 1) eq ':'
57 );
58 },
59 'number-equal?' => sub {
60 return $_[0] == $_[1];
61 },
62 'string-equal?' => sub {
63 return $_[0] eq $_[1];
64 },
65 'list' => sub { return \@_; },
66 'drop-first' => sub {
67 my ($args) = @_;
68 my ($first, @rest) = @$args;
69 return \@rest;
70 },
71 'list-length' => sub {
72 return scalar(@{$_[0]});
73 },
74 'string-length' => sub {
75 return length($_[0]);
76 },
77 'string-split' => sub {
78 my ($arg, $delimiter) = @_;
79 if (not defined($delimiter)) {
80 return [split('', $arg)];
81 }
82 else {
83 return [split($delimiter, $arg)];
84 }
85 },
86 'string-join' => sub {
87 my ($arg1, $arg2) = @_;
88 return $arg1 . $arg2;
89 },
90 'list-join' => sub {
91 my ($arg1, $arg2) = @_;
92 return [@$arg1, @$arg2];
93 },
94 'list-prepend' => sub {
95 my ($arg, $list) = @_;
96 my $list_copy = [@{$list}];
97 unshift(@$list_copy, $arg);
98 return $list_copy;
99 },
100 'list-append' => sub {
101 my ($arg, $list) = @_;
102 my $list_copy = [@{$list}];
103 push(@$list_copy, $arg);
104 return $list_copy;
105 },
106 'drop-last' => sub {
107 my ($list) = @_;
108 my $list_copy = [@{$list}];
109 pop(@$list_copy);
110 return $list_copy;
111 },
112 'add' => sub { return $_[0] + $_[1]; },
113 'subtract' => sub { return $_[0] - $_[1]; },
114 'multiply' => sub { return $_[0] * $_[1]; },
115 'divide' => sub { return $_[0] / $_[1]; },
116 'remainder' => sub { return $_[0] % $_[1]; },
117 'do' => sub { return $_[-1]; },
118 };
119
120 my $std_lib = <<STRING_BLOCK;
121 not sure if i want to implement the standard lib in a string block of separate file haha
122 STRING_BLOCK
123
124 sub zip {
125 my ($parameters, $args, $acc) = @_;
126 if (scalar(@$parameters) == 0) {
127 return $acc;
128 }
129 else {
130 my ($parameters_first, @parameters_rest) = @$parameters;
131 my ($args_first, @args_rest) = @$args;
132 $acc->{$parameters_first->{'value'}} = $args_first;
133 @_ = (\@parameters_rest, \@args_rest, $acc);
134 return goto &zip;
135 }
136 }
137
138 sub get {
139 my ($scope, $key) = @_;
140 # check if in local scope
141 if (defined($scope->{$key})) {
142 return $scope->{$key};
143 }
144 # recursively check parent scope
145 elsif ($scope->{'parent_scope'}) {
146 @_ = ($scope->{'parent_scope'}, $key);
147 return goto &get;
148 }
149 else {
150 print("error: '$key' isn't defined.\n\n");
151 print("you can define a symbol using the following syntax:\n\n");
152 die("(def $key \"hello world\")\n");
153 }
154 }
155
156 sub read_file {
157 my ($file) = @_;
158
159 my $file_contents = do {
160 local $/ = undef;
161 open(my $file_handle, "<", $file) or die "can't open $file: $!";
162 # we don't need to close this $file_handle. lexical file handles close
163 # when they go out of scope
164 <$file_handle>;
165 };
166
167 return $file_contents;
168 }
169
170 sub is_space {
171 my ($char) = @_;
172 return $char =~ /\s/
173 }
174
175 sub atomize {
176 my ($str) = @_;
177
178 if (looks_like_number($str)) {
179 {
180 'type' => AST_NUMBER,
181 'value' => $str,
182 }
183 }
184 elsif (':' eq substr($str, 0, 1)) {
185 {
186 'type' => AST_KEYWORD,
187 'value' => $str,
188 }
189 }
190 else {
191 {
192 'type' => AST_SYMBOL,
193 'value' => $str,
194 }
195 }
196 }
197
198 sub unescape {
199 my ($char, @chars) = @_;
200
201 if ($char eq '"') {
202 return ("\"", \@chars);
203 }
204 elsif ($char eq 'n') {
205 return ("\n", \@chars);
206 }
207 elsif ($char eq 't') {
208 return ("\t", \@chars);
209 }
210 else {
211 print("error: '\\$char' is not a valid escape code.\n\n");
212 print("only the following escape codes are valid:\n\n");
213 print("\\\" -> for double quotation marks.\n");
214 print("\\n -> for newlines.\n");
215 die("\\t -> for tabs\n");
216 }
217 }
218
219 sub unescape_string {
220 my ($str, $acc) = @_;
221 # this is messy, but it saves me from having to split the $str argument and
222 # provide a "" every time i want to call this function
223 if (ref($str) ne 'ARRAY') { $str = [split('', $str)]; }
224 if (not defined($acc)) { $acc = "" }
225 if (scalar(@$str) == 0) {
226 return $acc;
227 }
228 else {
229 my ($char, @rest_chars) = @$str;
230 if ($char eq '\\') {
231 my ($unescaped_char, $remaining_chars) = unescape(@rest_chars);
232 $acc = $acc . $unescaped_char;
233 @_ = ($remaining_chars, $acc);
234 return goto &unescape_string;
235 }
236 else {
237 $acc = $acc . $char;
238 @_ = (\@rest_chars, $acc);
239 return goto &unescape_string;
240 }
241 }
242 }
243
244 sub ignore_line {
245 my ($char, @rest_chars) = @_;
246
247 if ($char ne "\n") {
248 @_ = @rest_chars;
249 return goto &ignore_line;
250 }
251 else {
252 return \@rest_chars;
253 }
254 }
255
256 sub build_string {
257 my ($chars, $str) = @_;
258 my ($char, @rest_chars) = @$chars;
259
260 if ($char eq '\\') {
261 my ($unescaped_char, $remaining_chars) = unescape(@rest_chars);
262 $str = $str . $unescaped_char;
263
264 @_ = ($remaining_chars, $str);
265 return goto &build_string;
266 }
267 elsif ($char ne '"') {
268 my $new_str = $str . $char;
269
270 @_ = (\@rest_chars, $new_str);
271 return goto &build_string;
272 }
273 else {
274 my $return_str = {
275 'type' => AST_STRING,
276 'value' => $str,
277 };
278 return (\@rest_chars, $return_str);
279 }
280 }
281
282 sub build_atom {
283 my ($chars, $str) = @_;
284 my ($char, @rest_chars) = @$chars;
285
286 if (not is_space($char) and
287 $char ne ')' and
288 $char ne '(' and
289 $char ne '[' and
290 $char ne ']') {
291 my $new_str = $str . $char;
292
293 @_ = (\@rest_chars, $new_str);
294 return goto &build_atom;
295 }
296 else {
297 return ($chars, atomize($str));
298 }
299 }
300
301 sub eval_args {
302 my ($expr, $acc, $current_scope) = @_;
303
304 if (scalar(@$expr) == 0) {
305 return $acc;
306 }
307 else {
308 my ($first_expr, @rest_expr) = @$expr;
309 my ($result, $new_scope) = eval_single_expr($first_expr, $current_scope);
310
311 push(@$acc, $result);
312 @_ = (\@rest_expr, $acc, $new_scope);
313 return goto &eval_args;
314 }
315 }
316
317 sub lisp_string {
318 my ($expr, $mode) = @_;
319 if (not defined($mode)) {
320 $mode = 0;
321 }
322
323 # not actually sure if i ever return an undef here to be honest
324 if (not defined($expr)) {
325 return 'undef';
326 }
327 elsif ($expr eq "") {
328 return '""';
329 }
330 # functions
331 # this turns something like HASH(0x562aaeb46cc8) into
332 # function(0x562aaeb46cc8)
333 elsif (ref($expr) eq 'HASH') {
334 return $expr =~ s/HASH/function/r;
335 }
336 # lists
337 elsif (ref($expr) eq 'ARRAY') {
338 return "[" . join(" ", map { lisp_string($_, $mode); } @$expr) . "]";
339 }
340 # strings
341 elsif (not substr($expr, 0, 1) eq ':' and
342 $expr =~ /^[\s\S]+$/ and
343 not looks_like_number($expr)) {
344 if ($mode eq 'repl-mode') {
345 # can't get my escape_string and unescape_strings to work on this,
346 # so i'll just use a bunch of these? :shrug: :
347 #$expr =~ s/"/\\"/g;
348 #$expr =~ s/\n/\\n/g;
349 #$expr =~ s/\t/\\t/g;
350 return '"' . $expr . '"';
351 }
352 else {
353 return $expr;
354 }
355 }
356 # keywords, numbers, and quoted symbols
357 else {
358 return $expr;
359 }
360 }
361
362 sub lisp_string_quote {
363 my ($expr) = @_;
364
365 if (ref($expr) eq 'ARRAY') {
366 return "[" . join(" ", map { lisp_string_quote($_); } @$expr) . "]";
367 }
368 elsif (ref($expr) eq 'HASH') {
369 my $result = $expr->{'value'};
370 if ($result eq 'list') {
371 return;
372 }
373 elsif ($expr->{'type'} eq AST_STRING) {
374 return "\"$result\"";
375 }
376 else {
377 return $result;
378 }
379 }
380 else {
381 return $expr;
382 }
383 }
384
385 # reminder: all tokens in the AST are hashes containing the type and value
386 sub eval_single_expr {
387 my ($expr, $current_scope) = @_;
388
389 # ==============
390 # = not a list =
391 # ==============
392 if (ref($expr) eq 'HASH') {
393 # ------------
394 # - constant -
395 # ------------
396 if ($expr->{'type'} eq AST_NUMBER or
397 $expr->{'type'} eq AST_KEYWORD or
398 $expr->{'type'} eq AST_STRING) {
399 return ($expr->{'value'}, $current_scope);
400 }
401 # ----------
402 # - symbol -
403 # ----------
404 elsif ($expr->{'type'} eq AST_SYMBOL) {
405 return (get($current_scope, $expr->{'value'}), $current_scope);
406 }
407 }
408
409 # =============
410 # = is a list =
411 # =============
412 # ------------
413 # - () or [] -
414 # ------------
415 elsif (scalar(@$expr) == 0) {
416 return ($expr, $current_scope);
417 }
418 # ------------------
419 # - (print <expr>) -
420 # ------------------
421 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'print') {
422 my (undef, $arg) = @$expr;
423 my ($arg_evaluated, undef) = eval_single_expr($arg, $current_scope);
424 #print($arg_evaluated);
425 print($arg_evaluated);
426 return (OK, $current_scope);
427 }
428 # --------------------------
429 # - (load-file <filename>) -
430 # --------------------------
431 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'load-file') {
432 my (undef, $filename) = @$expr;
433 my ($filename_evaluated, undef) = eval_single_expr($filename, $current_scope);
434 # NEW (mayyybe) =============
435 # my $ast = make_ast(tokenize_file($filename_evaluated), []);
436 # # ignore return value
437 # my (undef, $new_scope) = eval_all_expr($ast, [], $current_scope);
438 # return (OK, $new_scope);
439 # OLD =======================
440 # ignore return value
441 my (undef, $new_scope) = load_file($filename_evaluated, $current_scope);
442 return (OK, $new_scope);
443 }
444 # ------------------------
445 # - (def <name> <value>) -
446 # ------------------------
447 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'def') {
448 my (undef, $name, $definition) = @$expr;
449 my ($definition_evaluated, undef) = eval_single_expr($definition, $current_scope);
450 # create new copy of current_scope
451 my $new_scope = {%{$current_scope}};
452 $new_scope->{$name->{'value'}} = $definition_evaluated;
453 return (OK, $new_scope);
454 }
455 # -----------------------------------------------
456 # - (defn <name> [<parameters> ...] <body ...>) -
457 # -----------------------------------------------
458 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'defn') {
459 my (undef, $symbol, $parameters, @body) = @$expr;
460 my $def = {'type' => AST_SYMBOL, 'value' => 'def'};
461 my $fn = {'type' => AST_SYMBOL, 'value' => 'fn'};
462 @_ = ([$def, $symbol, [$fn, $parameters, @body]], $current_scope);
463 return goto &eval_single_expr;
464 }
465 # --------------------------------------
466 # - (fn [<parameters> ...] <body ...>) -
467 # --------------------------------------
468 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'fn') {
469 my (undef, $args, @body) = @$expr;
470 my (undef, @params) = @$args;
471 # we don't turn body into a reference here because it was already turned
472 # into a reference by the ast
473 return ({'parameters' => \@params, 'body' => \@body}, $current_scope);
474 }
475 # -----------------------------
476 # - (if <cond> <then> <else>) -
477 # -----------------------------
478 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'value'} eq 'if') {
479 my (undef, $condition, $then, $otherwise) = @$expr;
480 my ($condition_evaluated, undef) = eval_single_expr($condition, $current_scope);
481
482 if ($condition_evaluated) {
483 my ($then_evaluated, undef) = eval_single_expr($then, $current_scope);
484 return($then_evaluated, $current_scope);
485 }
486 else {
487 my ($otherwise_evaluated, undef) = eval_single_expr($otherwise, $current_scope);
488 return($otherwise_evaluated, $current_scope);
489 }
490 }
491 # ---------------------
492 # - (<number> <list>) -
493 # ---------------------
494 elsif (ref($expr->[0]) eq 'HASH' and $expr->[0]->{'type'} eq AST_NUMBER) {
495 my ($func, $arg) = @$expr;
496 my $number = $func->{'value'};
497 my ($arg_evaluated, undef) = eval_single_expr($arg, $current_scope);
498
499 if (ref($arg_evaluated) eq 'ARRAY') {
500 return ($arg_evaluated->[$number], $current_scope);
501 }
502 else {
503 my $str_invalid_expr = lisp_string_quote($expr);
504 print("error: '$arg_evaluated' is not a list in the expression '$str_invalid_expr`.\n\n");
505 print("try using a list or a definition containing a list, instead of '$arg_evaluated', similar to the examples below:\n\n");
506 print("(def my-list [1 2 3])\n");
507 print("(2 my-list)\n");
508 die("(2 [3 4 5])\n");
509 }
510 }
511 # --------------------
512 # - (<function> ...) -
513 # --------------------
514 else {
515 my ($head, @tail) = @$expr;
516 my ($head_evaluated, undef) = eval_single_expr($head, $current_scope);
517 my $args_evaluated = eval_args(\@tail, [], $current_scope);
518
519 # functions
520 if (ref($head_evaluated) eq 'CODE') {
521 return ($head_evaluated->(@$args_evaluated), $current_scope);
522 }
523 # anonymous functions
524 else {
525 # handle error for invalid function
526 if (ref($head_evaluated) ne 'HASH') {
527 my $str_invalid = lisp_string_quote($head);
528 my $str_invalid_expr = lisp_string_quote($expr);
529 print("'$str_invalid' isn't a valid function in the expression '$str_invalid_expr'.\n\n");
530 print("make sure '$str_invalid' isn't a list, string, or keyword. ");
531 print("if '$str_invalid' isn't a list, string, or keyword, then make sure that you've defined it.\n\n");
532 print("examples of invalid functions:\n\n");
533 print("(:example \"hello world\")\n");
534 print("(\"example\" \"hello world\")\n");
535 die("([1 2 3] \"hello world\")\n");
536 }
537 else {
538 my $parameters = $head_evaluated->{'parameters'};
539 my $fn_scope = zip($parameters, $args_evaluated, {});
540 $fn_scope->{'parent_scope'} = $current_scope;
541 my ($fn_body_result, undef) = eval_all_expr($head_evaluated->{'body'}, [], $fn_scope);
542
543 return ($fn_body_result, $current_scope);
544
545 }
546 }
547 }
548 }
549
550 sub make_ast {
551 my ($tokens, $ast) = @_;
552
553 if (scalar(@$tokens) == 0) {
554 return $ast;
555 }
556
557 my ($tok, @rest_tokens) = @$tokens;
558
559 if ($tok eq '(') {
560 my ($remaining_tokens, $sub_list) = make_ast(\@rest_tokens, []);
561
562 push(@$ast, $sub_list);
563 @_ = ($remaining_tokens, $ast);
564 return goto &make_ast;
565 }
566 elsif ($tok eq ')') {
567 return (\@rest_tokens, $ast);
568 }
569 # turn [1 2 3] into (list 1 2 3), ["list", 1, 2, 3] in perl, under the hood
570 elsif ($tok eq '[') {
571 my @sub_ast = {
572 'type' => AST_SYMBOL,
573 'value' => 'list'
574 };
575 my ($remaining_tokens, $sub_list) = make_ast(\@rest_tokens, \@sub_ast);
576
577 push(@$ast, $sub_list);
578 @_ = ($remaining_tokens, $ast);
579 return goto &make_ast;
580 }
581 elsif ($tok eq ']') {
582 return (\@rest_tokens, $ast);
583 }
584 else {
585 push(@$ast, $tok);
586 @_ = (\@rest_tokens, $ast);
587 return goto &make_ast;
588 }
589 }
590
591 sub tokenize {
592 my ($chars, $tokens) = @_;
593
594 if (scalar(@$chars) == 0) {
595 return $tokens;
596 }
597
598 my ($char, @rest_chars) = @$chars;
599
600 if (is_space($char)) {
601 @_ = (\@rest_chars, $tokens);
602 return goto &tokenize;
603 }
604 elsif ($char eq ';') {
605 my ($remaining_chars) = ignore_line(@rest_chars);
606 @_ = ($remaining_chars, $tokens);
607 return goto &tokenize;
608 }
609 elsif ($char eq '(' or
610 $char eq ')' or
611 $char eq '[' or
612 $char eq ']') {
613 push(@$tokens, $char);
614 @_ = (\@rest_chars, $tokens);
615 return goto &tokenize;
616 }
617 elsif ($char eq '"') {
618 my ($remaining_chars, $built_string) = build_string(\@rest_chars, "");
619
620 push(@$tokens, $built_string);
621 @_ = ($remaining_chars, $tokens);
622 return goto &tokenize;
623 }
624 else {
625 my ($remaining_chars, $built_atom) = build_atom($chars, "");
626
627 push(@$tokens, $built_atom);
628 @_ = ($remaining_chars, $tokens);
629 return goto &tokenize;
630 }
631 }
632
633 sub tokenize_file {
634 my ($file) = @_;
635
636 my $file_contents = read_file($file);
637 # don't need to do this awful trick below anymore, since i made
638 # eval_all_expr, which is basically a (do ...) statement. keeping this
639 # around for now, because it was a fun memory.
640 # my @split_contents = split('', '(do' . $file_contents . ')');
641 my $tokens = tokenize([split('', $file_contents)], []);
642
643 return $tokens;
644 }
645
646 sub eval_all_expr {
647 my ($expr, $result, $current_scope) = @_;
648 if (scalar(@$expr) == 0) {
649 return ($result, $current_scope);
650 }
651 else {
652 my ($first_expr, @rest_expr) = @$expr;
653 my ($new_result, $new_scope) = eval_single_expr($first_expr, $current_scope);
654 @_ = (\@rest_expr, $new_result, $new_scope);
655 return goto &eval_all_expr;
656 }
657 }
658
659 sub load_file {
660 my ($filename, $current_scope) = @_;
661 my $ast = make_ast(tokenize_file($filename), []);
662 # returns last expression evaluated, and a scope
663 return eval_all_expr($ast, [], $current_scope);
664 }
665
666 sub repl {
667 my ($scope) = @_;
668 print("> ");
669 my $input = <STDIN>;
670
671 # handle ctrl-d/ctrl-c
672 if (not defined($input)) {
673 exit();
674 }
675 # handle space input
676 elsif ($input =~ /^[\s]+$/) {
677 goto &repl;
678 }
679
680 eval {
681 # keep make_ast in here, because it can throw errors too
682 my $ast = make_ast(tokenize([split('', $input)]), []);
683 my ($result, $new_scope) = eval_all_expr($ast, [], $scope);
684 print(lisp_string($result, 'repl-mode') . "\n");
685 @_ = ($new_scope);
686 } or do {
687 my $error= $@ || "error";
688 print($error);
689 };
690
691 goto &repl;
692 }
693
694 sub main{
695 my $environment_copy = {%{$environment}};
696 if (scalar(@ARGV) < 1) {
697 print("welcome to lip, the really shitty lisp\n");
698 return repl($environment_copy);
699 }
700 else {
701 # === normal ===================================================
702 # old =====
703 #my $ast = make_ast(tokenize_file($ARGV[0]), []);
704 #return eval_all_expr($ast, [], $environment_copy);
705 # new =====
706 # list all built-in functions, and std.lip functions
707 if ($ARGV[0] eq 'functions') {
708 my (undef, $new_scope) = load_file("std.lip", $environment_copy);
709 for (sort(keys %$new_scope)) {
710 print($_ . "\n");
711 }
712 }
713 else {
714 return load_file($ARGV[0], $environment_copy);
715 }
716 # === end normal ===================================================
717
718 # === begin random test ========================================
719 # my $ast = make_ast(tokenize_file($ARGV[0]), []);
720 # my ($result, $env) = eval_all_expr($ast, [], $environment_copy);
721 # ##pp($env);
722 # print(lisp_string($result), "\n");
723 # === end random test ==========================================
724 }
725 }
726
727 main();