Implemented file device, simplified highlighting, added example.
h3rald h3rald@h3rald.com
Mon, 09 Feb 2026 14:59:46 +0100
10 files changed,
601 insertions(+),
31 deletions(-)
M
Makefile
→
Makefile
@@ -1,8 +1,8 @@
CC = gcc CFLAGS = -Wall -Wextra -g -SOURCES = xyw.c xywasm.c xywrun.c devices/system.c devices/terminal.c devices/clock.c -OBJS = xyw.o xywasm.o xywrun.o devices/system.o devices/terminal.o devices/clock.o +SOURCES = xyw.c xywasm.c xywrun.c devices/system.c devices/terminal.c devices/clock.c devices/file.c +OBJS = xyw.o xywasm.o xywrun.o devices/system.o devices/terminal.o devices/clock.o devices/file.o .PHONY: clean@@ -26,6 +26,9 @@ $(CC) $(CFLAGS) -c devices/terminal.c -o devices/terminal.o
devices/clock.o: devices/clock.c xyw.h $(CC) $(CFLAGS) -c devices/clock.c -o devices/clock.o + +devices/file.o: devices/file.c xyw.h + $(CC) $(CFLAGS) -c devices/file.c -o devices/file.o clean: rm -f xyw xyw.exe $(OBJS)
A
devices/file.c
@@ -0,0 +1,524 @@
+#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <errno.h> + +#ifdef _WIN32 +#include <windows.h> +#include <direct.h> +#define PATH_SEP '\\' +#else +#include <dirent.h> +#include <unistd.h> +#define PATH_SEP '/' +#endif + +#include "../xyw.h" + +extern xyw_byte xyw_memory[]; + +/* file device */ +#define FILE_PATH 0x0 +#define FILE_TYPE 0x2 +#define FILE_SIZE 0x3 +#define FILE_SUCCESS 0x5 +#define FILE_LENGTH 0x7 +#define FILE_READ 0x9 +#define FILE_WRITE 0xB +#define FILE_OPERATION 0xD + +/* File operations */ +#define FILE_OP_READ 0x01 +#define FILE_OP_WRITE 0x02 +#define FILE_OP_APPEND 0x03 +#define FILE_OP_DELETE 0x04 + +/* File types */ +#define XYW_FILE_TYPE_UNKNOWN 0x00 +#define FILE_TYPE_FILE 0x01 +#define FILE_TYPE_DIRECTORY 0x02 + +/* Error codes (high nibble = 3 for file device) */ +#define FILE_ERROR_NOT_FOUND 0x31 +#define FILE_ERROR_ACCESS_DENIED 0x32 +#define FILE_ERROR_INVALID_OP 0x33 +#define FILE_ERROR_IO_ERROR 0x34 + +/* Maximum path length */ +#define FILE_MAX_PATH 256 + +/* Internal state for streaming */ +static FILE *file_handle = NULL; +static char file_current_path[FILE_MAX_PATH] = {0}; +static long file_cursor = 0; +static int file_is_write_mode = 0; + +/* Get null-terminated string from memory at given address */ +static void get_path_string(xyw_word addr, char *buf, size_t bufsize) +{ + size_t i; + for (i = 0; i < bufsize - 1 && xyw_memory[addr + i] != 0; i++) + { + buf[i] = xyw_memory[addr + i]; + } + buf[i] = '\0'; +} + +/* Close any open file */ +static void file_close(void) +{ + if (file_handle) + { + fclose(file_handle); + file_handle = NULL; + } + file_current_path[0] = '\0'; + file_cursor = 0; + file_is_write_mode = 0; +} + +/* Update file type and size based on path */ +static void update_file_stats(xyw_byte *data) +{ + xyw_word path_addr = XYW_PEEKW(&data[FILE_PATH]); + char path[FILE_MAX_PATH]; + struct stat st; + + if (path_addr == 0) + { + data[FILE_TYPE] = XYW_FILE_TYPE_UNKNOWN; + XYW_POKEW(&data[FILE_SIZE], 0); + return; + } + + get_path_string(path_addr, path, FILE_MAX_PATH); + + if (stat(path, &st) != 0) + { + data[FILE_TYPE] = XYW_FILE_TYPE_UNKNOWN; + XYW_POKEW(&data[FILE_SIZE], 0); + return; + } + + if (S_ISDIR(st.st_mode)) + { + data[FILE_TYPE] = FILE_TYPE_DIRECTORY; + XYW_POKEW(&data[FILE_SIZE], 0); + } + else if (S_ISREG(st.st_mode)) + { + data[FILE_TYPE] = FILE_TYPE_FILE; + /* Cap size at 64KB (0xFFFF) */ + xyw_word size = (st.st_size > 0xFFFF) ? 0xFFFF : (xyw_word)st.st_size; + XYW_POKEW(&data[FILE_SIZE], size); + } + else + { + data[FILE_TYPE] = XYW_FILE_TYPE_UNKNOWN; + XYW_POKEW(&data[FILE_SIZE], 0); + } +} + +/* Read file contents into memory buffer with cursor support */ +static void file_read(xyw_byte *data, xyw_byte *error) +{ + xyw_word path_addr = XYW_PEEKW(&data[FILE_PATH]); + xyw_word buffer_addr = XYW_PEEKW(&data[FILE_READ]); + xyw_word length = XYW_PEEKW(&data[FILE_LENGTH]); + char path[FILE_MAX_PATH]; + + /* Clear success */ + XYW_POKEW(&data[FILE_SUCCESS], 0); + + if (path_addr == 0 || buffer_addr == 0 || length == 0) + { + xyw_dbg(" Invalid parameters for file_read\n"); + *error = FILE_ERROR_INVALID_OP; + return; + } + + get_path_string(path_addr, path, FILE_MAX_PATH); + + xyw_dbg(" Reading file: %s, length=%u, cursor=%ld\n", path, length, file_cursor); + + /* Check if it's a directory - list contents (no cursor support for dirs) */ + if (data[FILE_TYPE] == FILE_TYPE_DIRECTORY) + { + xyw_word offset = 0; + +#ifdef _WIN32 + WIN32_FIND_DATAA findData; + HANDLE hFind; + char searchPath[FILE_MAX_PATH + 3]; + + snprintf(searchPath, sizeof(searchPath), "%s\\*", path); + hFind = FindFirstFileA(searchPath, &findData); + + if (hFind == INVALID_HANDLE_VALUE) + { + xyw_dbg(" Failed to open directory: %s\n", path); + *error = FILE_ERROR_ACCESS_DENIED; + return; + } + + /* Output . and .. first */ + char line[FILE_MAX_PATH + 16]; + int len = snprintf(line, sizeof(line), "%02X %04X .\n", FILE_TYPE_DIRECTORY, 0); + if (len <= (int)length) + { + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } + len = snprintf(line, sizeof(line), "%02X %04X ..\n", FILE_TYPE_DIRECTORY, 0); + if (offset + len <= length) + { + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } + + do + { + if (strcmp(findData.cFileName, ".") == 0 || + strcmp(findData.cFileName, "..") == 0) + { + continue; + } + + xyw_byte type = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + ? FILE_TYPE_DIRECTORY + : FILE_TYPE_FILE; + xyw_word size = 0; + if (type == FILE_TYPE_FILE) + { + size = (findData.nFileSizeLow > 0xFFFF) ? 0xFFFF : (xyw_word)findData.nFileSizeLow; + } + + len = snprintf(line, sizeof(line), "%02X %04X %s\n", type, size, findData.cFileName); + + if (offset + len > length) + break; + + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } while (FindNextFileA(hFind, &findData) != 0); + + FindClose(hFind); +#else + DIR *dir = opendir(path); + struct dirent *entry; + struct stat st; + char full_path[FILE_MAX_PATH * 2]; + + if (!dir) + { + xyw_dbg(" Failed to open directory: %s\n", path); + *error = FILE_ERROR_ACCESS_DENIED; + return; + } + + /* Output . and .. first */ + char line[FILE_MAX_PATH + 16]; + int len = snprintf(line, sizeof(line), "%02X %04X .\n", FILE_TYPE_DIRECTORY, 0); + if (len <= (int)length) + { + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } + len = snprintf(line, sizeof(line), "%02X %04X ..\n", FILE_TYPE_DIRECTORY, 0); + if (offset + len <= length) + { + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } + + while ((entry = readdir(dir)) != NULL) + { + if (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0) + { + continue; + } + + snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); + + xyw_byte type = XYW_FILE_TYPE_UNKNOWN; + xyw_word size = 0; + + if (stat(full_path, &st) == 0) + { + if (S_ISDIR(st.st_mode)) + { + type = FILE_TYPE_DIRECTORY; + } + else if (S_ISREG(st.st_mode)) + { + type = FILE_TYPE_FILE; + size = (st.st_size > 0xFFFF) ? 0xFFFF : (xyw_word)st.st_size; + } + } + + len = snprintf(line, sizeof(line), "%02X %04X %s\n", type, size, entry->d_name); + + if (offset + len > length) + break; + + memcpy(&xyw_memory[buffer_addr + offset], line, len); + offset += len; + } + + closedir(dir); +#endif + + /* Null-terminate if space */ + if (offset < length) + { + xyw_memory[buffer_addr + offset] = 0; + } + + XYW_POKEW(&data[FILE_SUCCESS], offset); + return; + } + + /* Regular file read with cursor */ + + /* Open file if not already open or if path changed */ + if (!file_handle || strcmp(file_current_path, path) != 0 || file_is_write_mode) + { + file_close(); + file_handle = fopen(path, "rb"); + if (!file_handle) + { + xyw_dbg(" Failed to open file: %s\n", path); + *error = FILE_ERROR_NOT_FOUND; + return; + } + strncpy(file_current_path, path, FILE_MAX_PATH - 1); + file_current_path[FILE_MAX_PATH - 1] = '\0'; + file_cursor = 0; + file_is_write_mode = 0; + } + + /* Seek to cursor position */ + if (fseek(file_handle, file_cursor, SEEK_SET) != 0) + { + xyw_dbg(" Failed to seek in file: %s\n", path); + *error = FILE_ERROR_IO_ERROR; + return; + } + + /* Read data */ + size_t bytes_read = fread(&xyw_memory[buffer_addr], 1, length, file_handle); + + /* Advance cursor */ + file_cursor += bytes_read; + + /* Null-terminate if space and we read less than length */ + if (bytes_read < length) + { + xyw_memory[buffer_addr + bytes_read] = 0; + } + + xyw_dbg(" Read %zu bytes, cursor now at %ld\n", bytes_read, file_cursor); + XYW_POKEW(&data[FILE_SUCCESS], (xyw_word)bytes_read); +} + +/* Write memory buffer to file with cursor support */ +static void file_write_op(xyw_byte *data, xyw_byte *error, int append) +{ + xyw_word path_addr = XYW_PEEKW(&data[FILE_PATH]); + xyw_word buffer_addr = XYW_PEEKW(&data[FILE_WRITE]); + xyw_word length = XYW_PEEKW(&data[FILE_LENGTH]); + char path[FILE_MAX_PATH]; + + /* Clear success */ + XYW_POKEW(&data[FILE_SUCCESS], 0); + + if (path_addr == 0 || buffer_addr == 0 || length == 0) + { + xyw_dbg(" Invalid parameters for file_write\n"); + *error = FILE_ERROR_INVALID_OP; + return; + } + + get_path_string(path_addr, path, FILE_MAX_PATH); + + xyw_dbg(" Writing to file: %s (append=%d), length=%u, cursor=%ld\n", path, append, length, file_cursor); + + /* Open file if not already open, path changed, or mode changed */ + int need_reopen = !file_handle || + strcmp(file_current_path, path) != 0 || + !file_is_write_mode; + + if (need_reopen) + { + file_close(); + + /* For append, open in append mode; otherwise open for read/write or create */ + if (append) + { + file_handle = fopen(path, "ab"); + } + else + { + /* Try to open existing file for r+b, if fails create with wb */ + file_handle = fopen(path, "r+b"); + if (!file_handle) + { + file_handle = fopen(path, "wb"); + } + } + + if (!file_handle) + { + xyw_dbg(" Failed to open file for writing: %s (errno=%d: %s)\n", path, errno, strerror(errno)); + *error = FILE_ERROR_ACCESS_DENIED; + return; + } + + strncpy(file_current_path, path, FILE_MAX_PATH - 1); + file_current_path[FILE_MAX_PATH - 1] = '\0'; + file_is_write_mode = 1; + + if (append) + { + /* Seek to end for append */ + fseek(file_handle, 0, SEEK_END); + file_cursor = ftell(file_handle); + } + else + { + file_cursor = 0; + } + } + + /* Seek to cursor position (for non-append continuing writes) */ + if (!append && fseek(file_handle, file_cursor, SEEK_SET) != 0) + { + xyw_dbg(" Failed to seek in file: %s\n", path); + *error = FILE_ERROR_IO_ERROR; + return; + } + + /* Find actual content length (stop at null terminator if present) */ + xyw_word actual_length = 0; + while (actual_length < length && xyw_memory[buffer_addr + actual_length] != 0) + { + actual_length++; + } + + if (actual_length == 0) + { + xyw_dbg(" Empty content for file_write\n"); + return; + } + + /* Write data */ + size_t written = fwrite(&xyw_memory[buffer_addr], 1, actual_length, file_handle); + fflush(file_handle); + + /* Advance cursor */ + file_cursor += written; + + xyw_dbg(" Wrote %zu bytes (of %u max), cursor now at %ld\n", written, length, file_cursor); + XYW_POKEW(&data[FILE_SUCCESS], (xyw_word)written); + + /* Update stats after write */ + update_file_stats(data); +} + +/* Delete a file */ +static void file_delete(xyw_byte *data, xyw_byte *error) +{ + xyw_word path_addr = XYW_PEEKW(&data[FILE_PATH]); + char path[FILE_MAX_PATH]; + + /* Clear success */ + XYW_POKEW(&data[FILE_SUCCESS], 0); + + if (path_addr == 0) + { + xyw_dbg(" Invalid path address for file_delete\n"); + *error = FILE_ERROR_INVALID_OP; + return; + } + + get_path_string(path_addr, path, FILE_MAX_PATH); + + /* Close file if it's the one being deleted */ + if (file_handle && strcmp(file_current_path, path) == 0) + { + file_close(); + } + + xyw_dbg(" Deleting file: %s\n", path); + + if (remove(path) != 0) + { + xyw_dbg(" Failed to delete file: %s\n", path); + *error = FILE_ERROR_ACCESS_DENIED; + return; + } + + /* Success = 1 for successful delete */ + XYW_POKEW(&data[FILE_SUCCESS], 1); + + /* Update stats after delete */ + update_file_stats(data); +} + +void file_init(xyw_byte *data) +{ + /* Close any open file */ + file_close(); + + /* Clear all device memory */ + memset(data, 0, 16); +} + +xyw_byte file_input(xyw_byte *data, xyw_byte addr, xyw_byte *error) +{ + (void)error; + return data[addr]; +} + +void file_output(xyw_byte *data, xyw_byte addr, xyw_byte *error) +{ + /* When path is written, close current file, update stats, reset cursor */ + if (addr == FILE_PATH || addr == FILE_PATH + 1) + { + file_close(); + update_file_stats(data); + } + + /* When operation is written, execute it */ + if (addr == FILE_OPERATION) + { + xyw_byte op = data[FILE_OPERATION]; + + switch (op) + { + case FILE_OP_READ: + file_read(data, error); + break; + case FILE_OP_WRITE: + file_write_op(data, error, 0); + break; + case FILE_OP_APPEND: + file_write_op(data, error, 1); + break; + case FILE_OP_DELETE: + file_delete(data, error); + break; + default: + if (op != 0) + { + *error = FILE_ERROR_INVALID_OP; + } + break; + } + + /* Clear operation after execution */ + data[FILE_OPERATION] = 0; + } +}
A
devices/file.h
@@ -0,0 +1,5 @@
+#include "../xyw.h" + +void file_init(xyw_byte *data); +xyw_byte file_input(xyw_byte *data, xyw_byte addr, xyw_byte *error); +void file_output(xyw_byte *data, xyw_byte addr, xyw_byte *error);
A
examples/fileops.xyw
@@ -0,0 +1,33 @@
+.include "lib/macros.xyw" + +; main program + +; write test.txt +test.txt file.path STW +contents_write file.write STW +$0100 file.length STW +file.op.write file.operation STB + +; read test.txt +contents_read file.read STW +$0100 file.length STW +file.op.read file.operation STB + +; print read contents +contents_read puts JSRw + +; delete test.txt +file.op.delete file.operation STB + +; exit +$00 system.state STB + +.include "lib/puts.xyw" + +.label test.txt + .string "test.txt" + +.label contents_write + .string "This is a test file.\nIt has multiple lines.\n" + +.label contents_read
M
examples/lib/macros.xyw
→
examples/lib/macros.xyw
@@ -29,11 +29,12 @@
;file .macro file.path $30 .macro file.type $32 -.macro file.stats $33 -.macro file.operation $35 -.macro file.read $36 -.macro file.write $38 -.macro file.length $3A +.macro file.size $33 +.macro file.success $35 +.macro file.length $37 +.macro file.read $39 +.macro file.write $3B +.macro file.operation $3D ;beeper .macro beeper.state $40@@ -41,12 +42,32 @@ .macro beeper.samples $41
.macro beeper.n_samples $43 .macro beeper.on_stop $45 -;;; Other macros - +;;; Utilities .macro MOD DIV POP .macro LBYTE SWP POP .macro PUTC $11 STB + +;;; ASCII Characters .macro NL $0A PUTC .macro SP $20 PUTC + +;;; Argument separators .macro arg.sep $1E .macro arg.end $1D + +;;; Errors +.macro error.file.not_found $31 +.macro error.file.access_denied $32 +.macro error.file.invalid_op $33 +.macro error.file.io_error $34 + +;;; File operations +.macro file.op.read $01 +.macro file.op.write $02 +.macro file.op.append $03 +.macro file.op.delete $04 + +;;; File types +.macro file.type.unspecified $00 +.macro file.type.file $01 +.macro file.type.directory $02
M
test.sh
→
test.sh
@@ -9,6 +9,7 @@ "run_test on-error.xyw assert_output_equals \$'Error: Division by zero.\\n'"
"run_test print-args.xyw test1 test2 -- assert_output_equals \$'print-args.xim, test1, test2\\n'" "run_test factorial8.xyw assert_output_equals \$'120\\n'" "run_test factorial16.xyw assert_output_equals \$'5040\\n'" + "run_test fileops.xyw assert_output_equals \$'This is a test file.\\nIt has multiple lines.\\n'" ) ### Helper functions
M
xyw-vscode/syntaxes/xyw.tmLanguage.json
→
xyw-vscode/syntaxes/xyw.tmLanguage.json
@@ -160,16 +160,6 @@ "name": "support.type.device.xyw",
"match": "\\b(system|terminal|file|clock|beeper|)\\.[a-zA-Z0-9_.\\-]*\\b" }, { - "comment": "Library subroutines", - "name": "support.class.subroutine.xyw", - "match": "\\b(putd|putdw|puts)\\b" - }, - { - "comment": "Other library macros", - "name": "support.class.macro.xyw", - "match": "\\b(LBYTE|SP|NL|PUTC|MOD|arg.sep|arg.end)\\b" - }, - { "comment": "Label/macro references: starts with letter/underscore, can contain letters, numbers, underscores, dashes, or dots", "name": "entity.name.function.xyw", "match": "\\b[a-zA-Z_][a-zA-Z0-9_.\\-]*\\b"
M
xyw.vim
→
xyw.vim
@@ -33,12 +33,6 @@
" Device identifiers (system.*, terminal.*, file.*, clock.*, beeper.*) syntax match xywDeviceId "\<\(system\|terminal\|file\|clock\|beeper\)\.[a-zA-Z0-9_.\-]*\>" -" Library subroutines -syntax match xywLibrary "\<\(putd\|putdw\|puts\)\>" - -" Library macros -syntax match xywLibrary "\<\(LBYTE\|SP\|NL\|MOD\|arg.end\|arg.sep\)\>" - " Instructions - BRK and RTS have no modifiers syntax match xywInstruction "\<\(BRK\|RTS\)\>"@@ -61,7 +55,6 @@ highlight default link xywEscape SpecialChar
highlight default link xywHexNumber Number highlight default link xywBinNumber Number highlight default link xywInstruction Statement -highlight default link xywLibrary Constant highlight default link xywDeviceId Type highlight default link xywIdentifier Identifier
M
xywasm.c
→
xywasm.c
@@ -1,9 +1,8 @@
#include <stdio.h> #include "xyw.h" -#define TOKEN_MAX_LENGTH 32 +#define TOKEN_MAX_LENGTH 256 #define DIRECTIVE_CONTENT_MAX_LENGTH 256 -#define STRING_MAX_LENGTH 256 #define MAX_SYMBOLS 0x500 #define MAX_REFS 0x1000@@ -815,7 +814,7 @@ token_column = ctx->column;
token_started = 1; } ctx->column++; - if (token_length > STRING_MAX_LENGTH) + if (token_length > TOKEN_MAX_LENGTH) { token_error("Token too long"); return TOKEN_END_INVALID;@@ -881,7 +880,7 @@ return TOKEN_END_INVALID;
} } ctx->column++; - if (token_length > STRING_MAX_LENGTH) + if (token_length > TOKEN_MAX_LENGTH) { token_error("Token too long"); return TOKEN_END_INVALID;
M
xywrun.c
→
xywrun.c
@@ -5,6 +5,7 @@
#include "devices/system.h" #include "devices/terminal.h" #include "devices/clock.h" +#include "devices/file.h" #define MEMORY_SIZE 0xffff #define STACK_SIZE 0xff@@ -293,7 +294,7 @@ {&xyw_memory[0xff10], 0, terminal_input, terminal_output},
// Clock device {&xyw_memory[0xff20], clock_init, clock_input, clock_output}, // File device - {&xyw_memory[0xff30], 0, 0, 0}, + {&xyw_memory[0xff30], file_init, file_input, file_output}, // ... }; // clang-format on