🐐 SCRIPTA
This commit is contained in:
10
README.md
10
README.md
@@ -368,6 +368,16 @@ When `_` is added _after_ a numeric symbol, the symbol becomes 1.000 times large
|
|||||||
|
|
||||||
All integer symbols except `I` may be given a `_`.
|
All integer symbols except `I` may be given a `_`.
|
||||||
|
|
||||||
|
### SCRIPTA
|
||||||
|
|
||||||
|
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE string`, `SCRIBE string string`, and `ADIVNGE string string`.
|
||||||
|
|
||||||
|
`LEGE string` reads the contents of the file at the given path and returns them as a string.
|
||||||
|
|
||||||
|
`SCRIBE string string` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
|
||||||
|
|
||||||
|
`ADIVNGE string string` appends the second argument to the file at the path given by the first argument.
|
||||||
|
|
||||||
### SVBNVLLA
|
### SVBNVLLA
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1152,6 +1152,28 @@ class BuiltIn(Node):
|
|||||||
raise CentvrionError("DORMI requires a number or NVLLVS")
|
raise CentvrionError("DORMI requires a number or NVLLVS")
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
return vtable, ValNul()
|
return vtable, ValNul()
|
||||||
|
case "LEGE":
|
||||||
|
if "SCRIPTA" not in vtable["#modules"]:
|
||||||
|
raise CentvrionError("Cannot use 'LEGE' without module 'SCRIPTA'")
|
||||||
|
path = make_string(params[0], magnvm, svbnvlla)
|
||||||
|
with open(path, "r") as f:
|
||||||
|
return vtable, ValStr(f.read())
|
||||||
|
case "SCRIBE":
|
||||||
|
if "SCRIPTA" not in vtable["#modules"]:
|
||||||
|
raise CentvrionError("Cannot use 'SCRIBE' without module 'SCRIPTA'")
|
||||||
|
path = make_string(params[0], magnvm, svbnvlla)
|
||||||
|
content = make_string(params[1], magnvm, svbnvlla)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return vtable, ValNul()
|
||||||
|
case "ADIVNGE":
|
||||||
|
if "SCRIPTA" not in vtable["#modules"]:
|
||||||
|
raise CentvrionError("Cannot use 'ADIVNGE' without module 'SCRIPTA'")
|
||||||
|
path = make_string(params[0], magnvm, svbnvlla)
|
||||||
|
content = make_string(params[1], magnvm, svbnvlla)
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(content)
|
||||||
|
return vtable, ValNul()
|
||||||
case _:
|
case _:
|
||||||
raise NotImplementedError(self.builtin)
|
raise NotImplementedError(self.builtin)
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,29 @@ def _emit_builtin(node, ctx):
|
|||||||
lines.append(f"cent_dormi({param_vars[0]});")
|
lines.append(f"cent_dormi({param_vars[0]});")
|
||||||
lines.append(f"CentValue {tmp} = cent_null();")
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
|
||||||
|
case "LEGE":
|
||||||
|
if not ctx.has_module("SCRIPTA"):
|
||||||
|
lines.append('cent_runtime_error("SCRIPTA module required for LEGE");')
|
||||||
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
else:
|
||||||
|
lines.append(f"CentValue {tmp} = cent_lege({param_vars[0]});")
|
||||||
|
|
||||||
|
case "SCRIBE":
|
||||||
|
if not ctx.has_module("SCRIPTA"):
|
||||||
|
lines.append('cent_runtime_error("SCRIPTA module required for SCRIBE");')
|
||||||
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
else:
|
||||||
|
lines.append(f"cent_scribe({param_vars[0]}, {param_vars[1]});")
|
||||||
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
|
||||||
|
case "ADIVNGE":
|
||||||
|
if not ctx.has_module("SCRIPTA"):
|
||||||
|
lines.append('cent_runtime_error("SCRIPTA module required for ADIVNGE");')
|
||||||
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
else:
|
||||||
|
lines.append(f"cent_adivnge({param_vars[0]}, {param_vars[1]});")
|
||||||
|
lines.append(f"CentValue {tmp} = cent_null();")
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise NotImplementedError(node.builtin)
|
raise NotImplementedError(node.builtin)
|
||||||
|
|
||||||
|
|||||||
@@ -576,6 +576,40 @@ void cent_dormi(CentValue n) {
|
|||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- SCRIPTA module ---- */
|
||||||
|
|
||||||
|
CentValue cent_lege(CentValue path) {
|
||||||
|
const char *p = cent_make_string(path);
|
||||||
|
FILE *f = fopen(p, "r");
|
||||||
|
if (!f) cent_runtime_error("LEGE: cannot open file");
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long len = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
char *buf = cent_arena_alloc(cent_arena, len + 1);
|
||||||
|
fread(buf, 1, len, f);
|
||||||
|
buf[len] = '\0';
|
||||||
|
fclose(f);
|
||||||
|
return cent_str(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cent_scribe(CentValue path, CentValue content) {
|
||||||
|
const char *p = cent_make_string(path);
|
||||||
|
const char *c = cent_make_string(content);
|
||||||
|
FILE *f = fopen(p, "w");
|
||||||
|
if (!f) cent_runtime_error("SCRIBE: cannot open file");
|
||||||
|
fputs(c, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cent_adivnge(CentValue path, CentValue content) {
|
||||||
|
const char *p = cent_make_string(path);
|
||||||
|
const char *c = cent_make_string(content);
|
||||||
|
FILE *f = fopen(p, "a");
|
||||||
|
if (!f) cent_runtime_error("ADIVNGE: cannot open file");
|
||||||
|
fputs(c, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
CentValue cent_fortis_numerus(CentValue lo, CentValue hi) {
|
CentValue cent_fortis_numerus(CentValue lo, CentValue hi) {
|
||||||
if (lo.type != CENT_INT || hi.type != CENT_INT)
|
if (lo.type != CENT_INT || hi.type != CENT_INT)
|
||||||
cent_type_error("'FORTIS_NVMERVS' requires two integers");
|
cent_type_error("'FORTIS_NVMERVS' requires two integers");
|
||||||
|
|||||||
@@ -223,6 +223,9 @@ CentValue cent_senatus(CentValue *args, int n); /* SENATVS */
|
|||||||
CentValue cent_typvs(CentValue v); /* TYPVS */
|
CentValue cent_typvs(CentValue v); /* TYPVS */
|
||||||
void cent_dormi(CentValue n); /* DORMI */
|
void cent_dormi(CentValue n); /* DORMI */
|
||||||
CentValue cent_ordina(CentValue lst); /* ORDINA */
|
CentValue cent_ordina(CentValue lst); /* ORDINA */
|
||||||
|
CentValue cent_lege(CentValue path); /* LEGE */
|
||||||
|
void cent_scribe(CentValue path, CentValue content); /* SCRIBE */
|
||||||
|
void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Array helpers */
|
/* Array helpers */
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ builtin_tokens = [("BUILTIN", i) for i in [
|
|||||||
"ORDINA",
|
"ORDINA",
|
||||||
"SEMEN",
|
"SEMEN",
|
||||||
"SENATVS",
|
"SENATVS",
|
||||||
"TYPVS"
|
"TYPVS",
|
||||||
|
"LEGE",
|
||||||
|
"SCRIBE",
|
||||||
|
"ADIVNGE"
|
||||||
]]
|
]]
|
||||||
|
|
||||||
data_tokens = [
|
data_tokens = [
|
||||||
@@ -65,6 +68,7 @@ module_tokens = [("MODULE", i) for i in [
|
|||||||
"FORS",
|
"FORS",
|
||||||
"FRACTIO",
|
"FRACTIO",
|
||||||
"MAGNVM",
|
"MAGNVM",
|
||||||
|
"SCRIPTA",
|
||||||
"SVBNVLLA"
|
"SVBNVLLA"
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
\newpage
|
\newpage
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
|
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
|
||||||
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features.
|
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals).
|
||||||
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
|
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
|
||||||
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
|
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
|
||||||
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal.
|
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal.
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ contexts:
|
|||||||
scope: constant.language.centvrion
|
scope: constant.language.centvrion
|
||||||
|
|
||||||
builtins:
|
builtins:
|
||||||
- match: '\b(AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DICE|EVERRO|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LONGITVDO|ORDINA|SEMEN|SENATVS)\b'
|
- match: '\b(ADIVNGE|AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DICE|EVERRO|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LEGE|LONGITVDO|ORDINA|SCRIBE|SEMEN|SENATVS)\b'
|
||||||
scope: support.function.builtin.centvrion
|
scope: support.function.builtin.centvrion
|
||||||
|
|
||||||
modules:
|
modules:
|
||||||
- match: '\b(FORS|FRACTIO|MAGNVM|SVBNVLLA)\b'
|
- match: '\b(FORS|FRACTIO|MAGNVM|SCRIPTA|SVBNVLLA)\b'
|
||||||
scope: support.class.module.centvrion
|
scope: support.class.module.centvrion
|
||||||
|
|
||||||
keywords:
|
keywords:
|
||||||
|
|||||||
150
tests.py
150
tests.py
@@ -655,6 +655,9 @@ error_tests = [
|
|||||||
("SENATVS(I)", CentvrionError), # SENATVS requires booleans
|
("SENATVS(I)", CentvrionError), # SENATVS requires booleans
|
||||||
("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types
|
("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types
|
||||||
("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools
|
("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools
|
||||||
|
('LEGE("x.txt")', CentvrionError), # SCRIPTA required for LEGE
|
||||||
|
('SCRIBE("x.txt", "hi")', CentvrionError), # SCRIPTA required for SCRIBE
|
||||||
|
('ADIVNGE("x.txt", "hi")', CentvrionError), # SCRIPTA required for ADIVNGE
|
||||||
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
|
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
|
||||||
("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int
|
("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int
|
||||||
("IIIS", CentvrionError), # fraction without FRACTIO module
|
("IIIS", CentvrionError), # fraction without FRACTIO module
|
||||||
@@ -2329,5 +2332,152 @@ class TestDormi(unittest.TestCase):
|
|||||||
os.unlink(tmp_bin_path)
|
os.unlink(tmp_bin_path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestScripta(unittest.TestCase):
|
||||||
|
def _run_scripta(self, source):
|
||||||
|
lexer = Lexer().get_lexer()
|
||||||
|
tokens = lexer.lex(source + "\n")
|
||||||
|
program = Parser().parse(tokens)
|
||||||
|
# printer round-trip
|
||||||
|
new_text = program.print()
|
||||||
|
new_tokens = Lexer().get_lexer().lex(new_text + "\n")
|
||||||
|
new_nodes = Parser().parse(new_tokens)
|
||||||
|
self.assertEqual(program, new_nodes, f"Printer test\n{source}\n{new_text}")
|
||||||
|
return program
|
||||||
|
|
||||||
|
def _compile_and_run(self, program):
|
||||||
|
c_source = compile_program(program)
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
|
||||||
|
tmp_c.write(c_source)
|
||||||
|
tmp_c_path = tmp_c.name
|
||||||
|
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
|
||||||
|
tmp_bin_path = tmp_bin.name
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}")
|
||||||
|
return proc.stdout
|
||||||
|
finally:
|
||||||
|
os.unlink(tmp_c_path)
|
||||||
|
os.unlink(tmp_bin_path)
|
||||||
|
|
||||||
|
def test_scribe_and_lege(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDICE(LEGE("{path}"))'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
captured = StringIO()
|
||||||
|
with patch("sys.stdout", captured):
|
||||||
|
program.eval()
|
||||||
|
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
|
||||||
|
with open(path) as f:
|
||||||
|
self.assertEqual(f.read(), "SALVE MVNDE")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_scribe_and_lege_compiled(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDICE(LEGE("{path}"))'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
output = self._compile_and_run(program)
|
||||||
|
self.assertEqual(output, "SALVE MVNDE\n")
|
||||||
|
with open(path) as f:
|
||||||
|
self.assertEqual(f.read(), "SALVE MVNDE")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_adivnge(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDICE(LEGE("{path}"))'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
captured = StringIO()
|
||||||
|
with patch("sys.stdout", captured):
|
||||||
|
program.eval()
|
||||||
|
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_adivnge_compiled(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDICE(LEGE("{path}"))'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
output = self._compile_and_run(program)
|
||||||
|
self.assertEqual(output, "SALVE MVNDE\n")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_scribe_overwrites(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "first")\nSCRIBE("{path}", "second")\nLEGE("{path}")'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
result = program.eval()
|
||||||
|
self.assertEqual(result, ValStr("second"))
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_lege_empty_file(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nLEGE("{path}")'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
result = program.eval()
|
||||||
|
self.assertEqual(result, ValStr(""))
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_lege_preexisting_content(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
|
||||||
|
f.write("hello from python")
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nLEGE("{path}")'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
result = program.eval()
|
||||||
|
self.assertEqual(result, ValStr("hello from python"))
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_scribe_returns_nullus(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nSCRIBE("{path}", "x")'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
result = program.eval()
|
||||||
|
self.assertEqual(result, ValNul())
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_adivnge_returns_nullus(self):
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
source = f'CVM SCRIPTA\nADIVNGE("{path}", "x")'
|
||||||
|
program = self._run_scripta(source)
|
||||||
|
result = program.eval()
|
||||||
|
self.assertEqual(result, ValNul())
|
||||||
|
finally:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user