Skip to content

Commit cce534d

Browse files
Add UPPER, LOWER, READFILE, WRITEFILE, EXISTFILE, RUN, ISINT, and ISSTR.
1 parent d49d3df commit cce534d

File tree

6 files changed

+213
-16
lines changed

6 files changed

+213
-16
lines changed
6.85 KB
Binary file not shown.

asmln.exe

2.97 KB
Binary file not shown.

interpreter.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import json
3+
import subprocess
34
import math
45
import os
56
import sys
@@ -286,13 +287,21 @@ def __init__(self) -> None:
286287
self._register_int_only("CLOG", 1, self._safe_clog)
287288
self._register_custom("INT", 1, 1, self._int_op)
288289
self._register_custom("STR", 1, 1, self._str_op)
290+
self._register_custom("UPPER", 1, 1, self._upper)
291+
self._register_custom("LOWER", 1, 1, self._lower)
289292
self._register_custom("MAIN", 0, 0, self._main)
290293
self._register_custom("IMPORT", 1, 1, self._import)
291294
self._register_custom("INPUT", 0, 0, self._input)
292295
self._register_custom("PRINT", 0, None, self._print)
293296
self._register_custom("ASSERT", 1, 1, self._assert)
294297
self._register_custom("DEL", 1, 1, self._delete)
295298
self._register_custom("EXIST", 1, 1, self._exist)
299+
self._register_custom("ISINT", 1, 1, self._isint)
300+
self._register_custom("ISSTR", 1, 1, self._isstr)
301+
self._register_custom("READFILE", 1, 1, self._readfile)
302+
self._register_custom("WRITEFILE", 2, 2, self._writefile)
303+
self._register_custom("EXISTFILE", 1, 1, self._existfile)
304+
self._register_custom("RUN", 1, 1, self._run)
296305
self._register_custom("EXIT", 0, 1, self._exit)
297306

298307
def _register_int_only(self, name: str, arity: int, func: Callable[..., int]) -> None:
@@ -540,6 +549,30 @@ def _str_op(self, _: "Interpreter", args: List[Value], __: List[Expression], ___
540549
return Value(TYPE_STR, "-" + format(-number, "b"))
541550
return Value(TYPE_STR, format(number, "b"))
542551

552+
def _upper(
553+
self,
554+
interpreter: "Interpreter",
555+
args: List[Value],
556+
__: List[Expression],
557+
___: Environment,
558+
location: SourceLocation,
559+
) -> Value:
560+
s = self._expect_str(args[0], "UPPER", location)
561+
# Convert ASCII letters to upper-case; other bytes are unchanged
562+
return Value(TYPE_STR, s.upper())
563+
564+
def _lower(
565+
self,
566+
interpreter: "Interpreter",
567+
args: List[Value],
568+
__: List[Expression],
569+
___: Environment,
570+
location: SourceLocation,
571+
) -> Value:
572+
s = self._expect_str(args[0], "LOWER", location)
573+
# Convert ASCII letters to lower-case; other bytes are unchanged
574+
return Value(TYPE_STR, s.lower())
575+
543576
def _safe_log(self, value: int) -> int:
544577
if value <= 0:
545578
raise ASMRuntimeError("LOG argument must be > 0", rewrite_rule="LOG")
@@ -712,6 +745,113 @@ def _exist(
712745
name = arg_nodes[0].name
713746
return Value(TYPE_INT, 1 if env.has(name) else 0)
714747

748+
def _isint(
749+
self,
750+
interpreter: "Interpreter",
751+
args: List[Value],
752+
arg_nodes: List[Expression],
753+
env: Environment,
754+
location: SourceLocation,
755+
) -> Value:
756+
# Expect an identifier node so we examine the symbol's binding and type
757+
if not arg_nodes or not isinstance(arg_nodes[0], Identifier):
758+
raise ASMRuntimeError("ISINT requires an identifier argument", location=location, rewrite_rule="ISINT")
759+
name = arg_nodes[0].name
760+
if not env.has(name):
761+
return Value(TYPE_INT, 0)
762+
try:
763+
val = env.get(name)
764+
except ASMRuntimeError as err:
765+
err.location = location
766+
raise
767+
return Value(TYPE_INT, 1 if val.type == TYPE_INT else 0)
768+
769+
def _isstr(
770+
self,
771+
interpreter: "Interpreter",
772+
args: List[Value],
773+
arg_nodes: List[Expression],
774+
env: Environment,
775+
location: SourceLocation,
776+
) -> Value:
777+
# Expect an identifier node so we examine the symbol's binding and type
778+
if not arg_nodes or not isinstance(arg_nodes[0], Identifier):
779+
raise ASMRuntimeError("ISSTR requires an identifier argument", location=location, rewrite_rule="ISSTR")
780+
name = arg_nodes[0].name
781+
if not env.has(name):
782+
return Value(TYPE_INT, 0)
783+
try:
784+
val = env.get(name)
785+
except ASMRuntimeError as err:
786+
err.location = location
787+
raise
788+
return Value(TYPE_INT, 1 if val.type == TYPE_STR else 0)
789+
790+
def _readfile(
791+
self,
792+
interpreter: "Interpreter",
793+
args: List[Value],
794+
__: List[Expression],
795+
___: Environment,
796+
location: SourceLocation,
797+
) -> Value:
798+
path = self._expect_str(args[0], "READFILE", location)
799+
try:
800+
with open(path, "r", encoding="utf-8") as handle:
801+
data = handle.read()
802+
except OSError as exc:
803+
raise ASMRuntimeError(f"Failed to read '{path}': {exc}", location=location, rewrite_rule="READFILE")
804+
return Value(TYPE_STR, data)
805+
806+
def _writefile(
807+
self,
808+
interpreter: "Interpreter",
809+
args: List[Value],
810+
__: List[Expression],
811+
___: Environment,
812+
location: SourceLocation,
813+
) -> Value:
814+
blob = self._expect_str(args[0], "WRITEFILE", location)
815+
path = self._expect_str(args[1], "WRITEFILE", location)
816+
try:
817+
with open(path, "w", encoding="utf-8") as handle:
818+
handle.write(blob)
819+
except OSError:
820+
return Value(TYPE_INT, 0)
821+
return Value(TYPE_INT, 1)
822+
823+
def _existfile(
824+
self,
825+
interpreter: "Interpreter",
826+
args: List[Value],
827+
__: List[Expression],
828+
___: Environment,
829+
location: SourceLocation,
830+
) -> Value:
831+
path = self._expect_str(args[0], "EXISTFILE", location)
832+
return Value(TYPE_INT, 1 if os.path.exists(path) else 0)
833+
834+
def _run(
835+
self,
836+
interpreter: "Interpreter",
837+
args: List[Value],
838+
__: List[Expression],
839+
___: Environment,
840+
location: SourceLocation,
841+
) -> Value:
842+
# Execute a shell command and return its exit code as INT.
843+
cmd = self._expect_str(args[0], "RUN", location)
844+
try:
845+
# Use the system shell to run the provided command string.
846+
completed = subprocess.run(cmd, shell=True)
847+
code = completed.returncode
848+
except OSError as exc:
849+
raise ASMRuntimeError(f"RUN failed: {exc}", location=location, rewrite_rule="RUN")
850+
# Record the run event for deterministic logging/replay
851+
interpreter.io_log.append({"event": "RUN", "cmd": cmd, "code": code})
852+
# Normalize return to INT
853+
return Value(TYPE_INT, int(code))
854+
715855
def _exit(
716856
self,
717857
interpreter: "Interpreter",

lib/prime.asmln

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ FUNC IS_PRIME(INT:n):INT{
88
IF(EQ(n,10)){
99
RETURN(1)
1010
}
11+
# reject even numbers greater than 2
12+
IF(EQ(MOD(n,10), 0)){
13+
RETURN(0)
14+
}
1115
INT: i = 11
12-
WHILE(LTE(POW(1,10))){
16+
WHILE(LTE(MUL(i,i), n)){
1317
IF(EQ(MOD(n,i),0)){
1418
RETURN(0)
1519
}
@@ -19,7 +23,7 @@ FUNC IS_PRIME(INT:n):INT{
1923
}
2024

2125
FUNC NEXT_PRIME(INT:start):INT{
22-
INT: n = start
26+
INT: n = ADD(start,1)
2327
WHILE(1){
2428
IF(IS_PRIME(n)){
2529
RETURN(n)
@@ -29,7 +33,7 @@ FUNC NEXT_PRIME(INT:start):INT{
2933
}
3034

3135
FUNC PREV_PRIME(INT:start):INT{
32-
INT: n = start
36+
INT: n = SUB(start,1)
3337
WHILE(GT(n,1)){
3438
IF(IS_PRIME(n)){
3539
RETURN(n)

spec.txt

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ characters, nor any of the following characters: '{', '}', '[', ']', '(',
8181
')', '=', ',', '#'. The first character of an identifier must not be the
8282
digit '0' or '1' (these digits are used to begin binary integer literals).
8383
However, the characters '0' and '1' are permitted in subsequent positions
84-
within an identifier (for example, `a01` and `X10Y` are valid identifiers,
85-
while `0foo` and `1bar` are not). The namespace is flat: variables and
84+
within an identifier (for example, 'a01' and 'X10Y' are valid identifiers,
85+
while '0foo' and '1bar' are not). The namespace is flat: variables and
8686
functions share a single identifier space, so a given name cannot
8787
simultaneously denote both. A user-defined function name must not conflict
8888
with the name of any built-in operator or function (see Section 4.1).
@@ -101,15 +101,15 @@ which match the reference lexer implementation:
101101
- Uppercase letters 'A'–'Z'
102102
- Decimal digits '2'–'9'
103103
- The punctuation and symbol characters
104-
`; / ! @ $ % & ~ _ + | < > ?`
104+
'; / ! @ $ % & ~ _ + | < > ?'
105105

106106
- Subsequent characters in an identifier may be any of the following:
107107

108108
- Lowercase letters 'a'–'z'
109109
- Uppercase letters 'A'–'Z'
110110
- Decimal digits '0'–'9'
111111
- The punctuation and symbol characters
112-
`; . / ! @ $ % & ~ _ + | < > ?`
112+
'; . / ! @ $ % & ~ _ + | < > ?'
113113

114114
As noted above, non-ASCII characters remain disallowed, and the
115115
delimiter characters '{', '}', '[', ']', '(', ')', '=', ',', and '#'
@@ -200,6 +200,15 @@ Conversion operators:
200200
- STR(n) converts an INT to its binary spelling as a STR (including a leading
201201
'-' for negative integers). Passing a STR returns it unchanged.
202202

203+
- UPPER(s) converts a STR by mapping all ASCII letters to their uppercase
204+
equivalents and returns the resulting STR. The argument must be a STR;
205+
passing an INT raises a runtime error. Non-letter characters are left
206+
unchanged.
207+
- LOWER(s) converts a STR by mapping all ASCII letters to their lowercase
208+
equivalents and returns the resulting STR. The argument must be a STR;
209+
passing an INT raises a runtime error. Non-letter characters are left
210+
unchanged.
211+
203212
Input, output, and assertions:
204213
- INPUT() returns a STR containing the raw input line (no implicit numeric
205214
conversion). The I/O event is recorded for replay.
@@ -211,6 +220,24 @@ Input, output, and assertions:
211220
non-empty string -> 1). If the result is 0, execution halts with an
212221
assertion failure.
213222

223+
File operations:
224+
- READFILE(path):STR — Reads the file located at 'path' and returns its
225+
contents as a STR. The argument 'path' must be a STR. If the file cannot be
226+
opened or read, the interpreter raises a runtime error (rewrite: READFILE).
227+
- WRITEFILE(blob, path):INT — Writes the string 'blob' to the file given by
228+
'path'. Both arguments must be STR. On success the call returns INT 1; on
229+
failure (for example, permission or I/O errors) it returns INT 0. The call
230+
does not raise on write failure.
231+
- EXISTFILE(path):INT — Returns INT 1 when a filesystem object exists at
232+
'path', otherwise returns INT 0. The argument must be a STR.
233+
- RUN(command):INT — Executes the provided 'command' string using the
234+
host shell and returns the subprocess exit code as an INT. The 'command'
235+
argument must be a STR. On a failure to start the subprocess (for example
236+
if the system cannot invoke a shell), the interpreter raises a runtime
237+
error (rewrite: RUN). The call records a 'RUN' event in the execution
238+
I/O log containing the command text and returned exit code, enabling
239+
deterministic logging and replay.
240+
214241
Other operators (unchanged): IMPORT, MAIN, DEL, EXIST, and EXIT retain their
215242
previous meanings but now operate in a typed environment.
216243

@@ -221,28 +248,28 @@ directory as the current source file. When the current source is provided
221248
via the '-source' string literal mode, the primary search directory is the
222249
process's current working directory. If the module file is not found in
223250
that location, the interpreter will additionally attempt to load the file
224-
from a `lib` subdirectory located alongside the interpreter implementation
251+
from a 'lib' subdirectory located alongside the interpreter implementation
225252
(that is, '<interpreter_dir>/lib/<name>.asmln', where '<interpreter_dir>' is
226253
the directory containing the interpreter script or executable).
227254

228255
The imported file is parsed and executed in its own isolated top-level
229256
environment: top-level assignments and function definitions in the imported
230257
module do not directly mutate the caller's environment. During execution
231-
inside the imported module, unqualified identifiers (for example, `x` or
232-
`helper`) refer to names in the module's own top-level namespace. Qualified
233-
identifiers (for example, `other.FOO`) refer only to the dotted names that
258+
inside the imported module, unqualified identifiers (for example, 'x' or
259+
'helper') refer to names in the module's own top-level namespace. Qualified
260+
identifiers (for example, 'other.FOO') refer only to the dotted names that
234261
the module itself has created or imported; in other words, a module may
235262
refer to its own imports via qualified names, and those qualified bindings
236263
are scoped to that module's namespace.
237264

238265
After the module finishes executing, each top-level binding exported by the
239266
module is made available to the importer under a dotted name formed by the
240-
module identifier and the exported symbol, for example `module.FOO` or
241-
`module.bar`. If the imported module itself imported other modules, those
267+
module identifier and the exported symbol, for example 'module.FOO' or
268+
'module.bar'. If the imported module itself imported other modules, those
242269
nested qualified bindings are preserved under the importing module's
243-
namespace (for example, `module.other.SYM` denotes a symbol that `module`
244-
imported from `other`). Callers must reference imported symbols using the
245-
qualified form (for example: `IMPORT(module)` followed by `module.FOO()`),
270+
namespace (for example, 'module.other.SYM' denotes a symbol that 'module'
271+
imported from 'other'). Callers must reference imported symbols using the
272+
qualified form (for example: 'IMPORT(module)' followed by 'module.FOO()'),
246273
which preserves a clear separation of namespaces between modules.
247274

248275
Each 'IMPORT' call executes the target module anew and then publishes its
@@ -765,6 +792,12 @@ Function / Operator Signatures (expression position)
765792
environment (searching enclosing environments), else '0'. The argument must
766793
be an identifier; supplying an expression that is not a plain identifier is a
767794
runtime error.
795+
- 'ISINT(x)' : returns '1' if the identifier 'x' exists and is of type INT,
796+
otherwise returns '0'. The argument must be an identifier; supplying any
797+
other expression is a runtime error.
798+
- 'ISSTR(x)' : returns '1' if the identifier 'x' exists and is of type STR,
799+
otherwise returns '0'. The argument must be an identifier; supplying any
800+
other expression is a runtime error.
768801

769802
Arithmetic (INT only)
770803
- 'ADD(a, b)' : a + b

test.asmln

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
IMPORT(collection)
22
IMPORT(prng)
33
IMPORT(csprng)
4+
IMPORT(prime)
45

56
FUNC RUN_TESTS():INT{
67
# Collection basics
@@ -43,6 +44,25 @@ FUNC RUN_TESTS():INT{
4344
INT: y = csprng.CS_PRNG_NEXT()
4445
ASSERT( EQ(x, y) )
4546

47+
# Prime library tests
48+
# Basic primality checks
49+
ASSERT( EQ( prime.IS_PRIME(10), 1 ) ) # 2 is prime
50+
ASSERT( EQ( prime.IS_PRIME(11), 1 ) ) # 3 is prime
51+
ASSERT( EQ( prime.IS_PRIME(101), 1 ) ) # 5 is prime
52+
53+
# NEXT_PRIME / PREV_PRIME
54+
ASSERT( EQ( prime.NEXT_PRIME(100), 101 ) ) # next prime after 4 is 5
55+
ASSERT( EQ( prime.PREV_PRIME(101), 11 ) ) # previous prime before 5 is 3
56+
57+
# Mersenne prime check (p=3 -> 2^3-1 = 7, prime)
58+
ASSERT( EQ( prime.IS_MERSENNE_PRIME(11), 1 ) )
59+
60+
# FACTOR: factor 10 (1010) -> [2 (10), 5 (101)]
61+
INT: factors = prime.FACTOR(1010)
62+
ASSERT( EQ( collection.GET_COUNT(factors), 10 ) )
63+
ASSERT( EQ( collection.COL_GET(factors, 0), 10 ) )
64+
ASSERT( EQ( collection.COL_GET(factors, 1), 101 ) )
65+
4666
# If we reach here all tests passed; print success code and return
4767
PRINT(1)
4868
RETURN(0)

0 commit comments

Comments
 (0)