Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jq: update interpreter, merge eval-ast, fix some env errors #685

Merged
merged 20 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions impls/jq/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:bionic
FROM ubuntu:24.04
MAINTAINER Joel Martin <[email protected]>

##########################################################
Expand All @@ -9,10 +9,8 @@ MAINTAINER Joel Martin <[email protected]>
RUN apt-get -y update

# Required for running tests
RUN apt-get -y install make python

# Some typical implementation and test requirements
RUN apt-get -y install curl libreadline-dev libedit-dev libpcre3-dev
RUN apt-get -y install make python3
RUN ln -fs /usr/bin/python3 /usr/local/bin/python

RUN mkdir -p /mal
WORKDIR /mal
Expand All @@ -21,12 +19,4 @@ WORKDIR /mal
# Specific implementation requirements
#########################################################

RUN apt-get -y install python3.8 wget
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.8 10

# grab jq 1.6 from github releases
RUN wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64

RUN chmod +x jq-linux64
# a bit ugly, but it'll do?
RUN mv jq-linux64 /usr/bin/jq
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install jq
10 changes: 9 additions & 1 deletion impls/jq/Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
all:

.PHONY: clean
clean:
rm -fr .mypy_cache/

check:
flake8 run
pylint run
mypy run

.PHONY: all clean check
29 changes: 27 additions & 2 deletions impls/jq/core.jq
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ include "reader";

def core_identify:
{
"+": {
kind: "fn", # native function
inputs: 2,
function: "number_add"
},
"-": {
kind: "fn", # native function
inputs: 2,
function: "number_sub"
},
"*": {
kind: "fn", # native function
inputs: 2,
function: "number_mul"
},
"/": {
kind: "fn", # native function
inputs: 2,
function: "number_div"
},
"eval": {
kind: "fn",
inputs: 1,
function: "eval"
},
"env": {
kind: "fn",
function: "env",
Expand Down Expand Up @@ -369,9 +394,9 @@ def core_interp(arguments; env):
) // (
select(.function == ">=") | null | wrap(arguments[0].value >= arguments[1].value | tostring)
) // (
select(.function == "slurp") | arguments | map(.value) | issue_extern("read") | wrap("string")
select(.function == "slurp") | arguments[0].value | slurp | wrap("string")
) // (
select(.function == "read-string") | arguments | first.value | read_str | read_form.value
select(.function == "read-string") | arguments | first.value | read_form
) // (
select(.function == "atom?") | null | wrap(arguments | first.kind == "atom" | tostring)
) // (
Expand Down
2 changes: 1 addition & 1 deletion impls/jq/env.jq
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def addToEnv(envexp; name):
def _env_remove_references(refs):
if . != null then
if .environment == null then
_debug("This one broke the rules, officer: \(.)")
debug("This one broke the rules, officer: \(.)")
else
{
environment: (.environment | to_entries | map(select(.key as $key | refs | contains([$key]) | not)) | from_entries),
Expand Down
2 changes: 1 addition & 1 deletion impls/jq/interp.jq
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def addFrees(newEnv; frees):
def interpret(arguments; env; _eval):
extractReplEnv(env) as $replEnv |
extractAtoms(env) as $envAtoms |
(if $DEBUG then _debug("INTERP: \(. | pr_str(env))") else . end) |
(if $DEBUG then debug("INTERP: \(pr_str(env))") end) |
(select(.kind == "fn") |
arg_check(arguments) |
(select(.function == "eval") |
Expand Down
2 changes: 1 addition & 1 deletion impls/jq/reader.jq
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,4 @@ def read_form_(depth):
end end end end end end end end end end);

def read_form:
{tokens: .} | read_form_(0);
({tokens: read_str} | read_form_(0).value) // {kind: "nil"};
112 changes: 0 additions & 112 deletions impls/jq/rts.py

This file was deleted.

53 changes: 51 additions & 2 deletions impls/jq/run
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
#!/bin/sh
#!/usr/bin/python3
"""Spawn a jq subprocess and wrap some IO interactions for it.

exec python rts.py "${@}"
jq seems unable to
- open an arbitrary file (slurp)
- emit a string on stdout without new line (readline)
"""
from json import JSONDecodeError, dumps, loads
from os import environ
from os.path import dirname, join, realpath
from subprocess import PIPE, Popen
from sys import argv

rundir = dirname(realpath(__file__))
with Popen(args=['/usr/bin/jq',
'--argjson', 'DEBUG', 'false',
'-nrM', # --null-input --raw-output --monochrome-output
'-L', rundir,
'-f', join(rundir, environ.get('STEP', 'stepA_mal') + '.jq'),
'--args'] + argv[1:],
stdin=PIPE, stderr=PIPE, encoding='utf-8',
) as proc:
assert proc.stderr is not None # for mypy
for received in proc.stderr:
try:
as_json = loads(received)
except JSONDecodeError:
print(f'JQ STDERR: {received}', end=None)
else:
match as_json:
case ['DEBUG:', ['display', str(message)]]:
# While at it, provide a way to immediately print to
# stdin for DEBUG-EVAL, println and prn (jq is able to
# output to stderr, but *we* are already piping it).
print(message)
# Jq waits for this signal to go on, so that its own
# output is not mixed with our one.
print('null', file=proc.stdin, flush=True)
case ['DEBUG:', ['readline', str(prompt)]]:
try:
data = input(prompt)
except EOFError:
break # Expected end of this script
print(dumps(data), file=proc.stdin, flush=True)
case ['DEBUG:', ['slurp', str(fname)]]:
with open(fname, 'r', encoding='utf-8') as file_handler:
data = file_handler.read()
print(dumps(data), file=proc.stdin, flush=True)
case _:
# Allow normal debugging information for other purposes.
print(f'JQ STDERR: {received}', end=None)
print()
17 changes: 4 additions & 13 deletions impls/jq/step0_repl.jq
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
include "utils";

def read_line:
. as $in
| label $top
| _readline;

def READ:
.;

Expand All @@ -14,14 +9,10 @@ def EVAL:
def PRINT:
.;

def rep:
READ | EVAL | PRINT | _display;

def repl_:
("user> " | _print) |
(read_line | rep);

def repl:
while(true; repl_);
# Infinite generator, interrupted by ./run.
"user> " | __readline |
READ | EVAL |
PRINT, repl;

repl
38 changes: 11 additions & 27 deletions impls/jq/step1_read_print.jq
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,25 @@ include "reader";
include "printer";
include "utils";

def read_line:
. as $in
| label $top
| _readline;

def READ:
read_str | read_form | .value;
read_form;

def EVAL:
.;

def PRINT:
pr_str;

def rep:
READ | EVAL |
if . != null then
PRINT
else
null
end;

def repl_:
("user> " | _print) |
(read_line | rep);

def repl:
{continue: true} | while(
.continue;
try {value: repl_, continue: true}
catch
if is_jqmal_error then
{value: "Error: \(.)", continue: true}
else
{value: ., continue: false}
end) | if .value then .value|_display else empty end;
# Infinite generator, interrupted by an exception or ./run.
"user> " | __readline |
try (
READ | EVAL |
PRINT, repl
) catch if is_jqmal_error then
., repl
else
halt_error
end;

repl
Loading