diff --git a/README.md b/README.md index 31e5bc4..7e8c52d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,24 @@ To run a program directly, pass the path to the `.clx` file as an argument: ./clyx path/to/program.clx ``` +### Self-contained Packaging (Compilation) + +The Go implementation supports reading an optional ZIP archive appended directly to the end of the `clyx` binary. This allows you to package your Clyx source code into a single executable file. + +To package your program: +1. Create a ZIP archive containing all your `.clx` source files. +2. Append the ZIP archive to the compiled `clyx` binary: + ```sh + cat clyx my_app.zip > my_app_executable + chmod +x my_app_executable + ``` + +When the packed executable runs: +- The interpreter automatically detects the appended ZIP archive. +- It iterates through all `.clx` files in the archive and evaluates (`src`) them. +- If a `_main` word is defined in the environment, it is executed automatically. +- If `_main` is not present, the interpreter falls back to running the standard REPL. + ## FAQ ### Where does the name Clyx originate from? diff --git a/clyx-go/clyx.go b/clyx-go/clyx.go index b10e2e9..579dccb 100644 --- a/clyx-go/clyx.go +++ b/clyx-go/clyx.go @@ -313,6 +313,12 @@ func (c *Clyx) Eval(q any) { } } +func (c *Clyx) HasWord(name string) bool { + _, ok := c.words[name] + return ok +} + + func printVal(v any) { switch val := v.(type) { case int64: fmt.Printf("%d\n", val) @@ -420,6 +426,8 @@ func NewClyx(libfname string, libCode string) *Clyx { c.words[".s"] = func() { printVal(c.reprStack()) } c.words["dip"] = func() { q, y := c.pop().([]any), c.pop(); c.execQuot(q); c.stack = append(c.stack, y) } c.words["loop"] = func() { body, cond := c.pop().([]any), c.pop().([]any); for { c.execQuot(cond); if !isTruthy(c.pop()) { break }; c.execQuot(body) } } + c.words["hsargs"] = func() { c.stack = append(c.stack, toAnySlice(os.Args)) } + c.words["hsexit"] = func() { os.Exit(int(toInt(c.pop()))) } c.words["hseval"] = func() { fmt.Fprintln(os.Stderr, "Error: unsupported word"); os.Exit(1) } c.words["chr"] = func() { c.stack = append(c.stack, string(rune(toInt(c.pop())))) } c.words["asc"] = func() { s := c.pop().(string); r, _ := utf8.DecodeRuneInString(s); c.stack = append(c.stack, int64(r)) } diff --git a/clyx-python/clyx/clyx.py b/clyx-python/clyx/clyx.py index 9db4552..5020652 100755 --- a/clyx-python/clyx/clyx.py +++ b/clyx-python/clyx/clyx.py @@ -62,6 +62,8 @@ class Clyx: 'delf': lambda: os.remove(self._stack.pop()), 'maked': lambda: os.mkdir(self._stack.pop()), 'listd': lambda: self._stack.append(os.listdir(self._stack.pop())), 'deld': lambda: os.rmdir(self._stack.pop()), 'rand': lambda: self._stack.append(randrange(int(self._stack.pop()))), '.s': lambda: self._out_num(repr(self._stack)), 'dip': lambda: (lambda q, y: (self._exec_quot(q), self._stack.append(y)))(self._stack.pop(), self._stack.pop()), 'loop': lambda: (lambda body, cond: self._exec_loop(cond, body))(self._stack.pop(), self._stack.pop()), + 'hsargs': lambda: self._stack.append(list(sys.argv)), + 'hsexit': lambda: sys.exit(int(self._stack.pop())), 'hseval': lambda: self._stack.append(eval(self._stack.pop())), 'chr': lambda: self._stack.append(chr(int(self._stack.pop()))), 'asc': lambda: self._stack.append(ord(self._stack.pop())), 'sin': lambda: self._stack.append(math.sin(float(self._stack.pop()))), 'cos': lambda: self._stack.append(math.cos(float(self._stack.pop()))), 'atan': lambda: self._stack.append(math.atan(float(self._stack.pop()))), diff --git a/clyx-python/pyproject.toml b/clyx-python/pyproject.toml index 1ed1cd4..307cc7d 100644 --- a/clyx-python/pyproject.toml +++ b/clyx-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "clyx" -version = "0.1.1" +version = "0.2" description = "Clyx programming language interpreter" readme = "README.md" requires-python = ">=3.7" diff --git a/clyx_manual.md b/clyx_manual.md index 5a73bb0..a0fa927 100644 --- a/clyx_manual.md +++ b/clyx_manual.md @@ -282,6 +282,14 @@ Any line fragment starting with `#` to the end of the physical source line is re - **Stack Effect**: `( -- )` - **Description**: Prints the current stack representation to standard output (useful for debugging). +### `hsargs` (Host Arguments) +- **Stack Effect**: `( -- [args] )` +- **Description**: Pushes a list/quotation of all passed command line arguments onto the stack. + +### `hsexit` (Host Exit) +- **Stack Effect**: `( code -- )` +- **Description**: Exits the program/interpreter with the specified exit code. + ### `hseval` (Host Language Evaluation) - **Stack Effect**: `( expr -- val )` - **Description**: Pops the string `expr` from the stack, evaluates it as an expression in the host language (e.g., Python), and pushes the resulting value back onto the stack. diff --git a/main.go b/main.go index a87c890..d6b3a51 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,12 @@ package main import ( + "archive/zip" _ "embed" "fmt" + "io" "os" + "strings" clyx "codeberg.org/luxferre/clyx/clyx-go" ) @@ -17,7 +20,73 @@ var defaultLibCode string //go:embed clyx_repl.clx var defaultReplCode string +func runZipArchive(zr *zip.Reader) error { + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", r) + os.Exit(1) + } + }() + + c := clyx.NewClyx("", defaultLibCode) + + for _, file := range zr.File { + if !file.FileInfo().IsDir() && strings.HasSuffix(strings.ToLower(file.Name), ".clx") { + rc, err := file.Open() + if err != nil { + return fmt.Errorf("error opening %s in archive: %w", file.Name, err) + } + data, err := io.ReadAll(rc) + rc.Close() + if err != nil { + return fmt.Errorf("error reading %s in archive: %w", file.Name, err) + } + c.Run(string(data)) + } + } + + if c.HasWord("_main") { + c.Run("_main") + } else { + c.Run(defaultReplCode) + } + return nil +} + +func detectAndRunZip() bool { + exePath, err := os.Executable() + if err != nil { + return false + } + f, err := os.Open(exePath) + if err != nil { + return false + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return false + } + + zr, err := zip.NewReader(f, info.Size()) + if err != nil { + return false // No ZIP appended + } + + // ZIP found! Run it + if err := runZipArchive(zr); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return true +} + func main() { + if detectAndRunZip() { + return + } + if len(os.Args) > 1 { clyx.RunFile(os.Args[1], defaultLibCode) } else { diff --git a/test.clx b/test.clx index 69e10fc..84f2d25 100755 --- a/test.clx +++ b/test.clx @@ -215,3 +215,13 @@ my_inline 100 = if [ " :: (inline) PASS" puts cr ] [ " :: (inline) FAIL" puts # " hseval FAIL" puts cr # ] +# 12. Host Arguments & Host Exit +"Testing host arguments and exit..." puts cr +hsargs isquot [ hsargs qlen 0 > ] and if [ + " hsargs PASS" puts cr +] [ + " hsargs FAIL" puts cr +] + +0 hsexit +