From 8100f5aae1a1e537867f21d0b849c5d0c94178af Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 18 Mar 2026 21:13:22 +0100 Subject: [PATCH] Ufff, I forgot to add the primay code... :) (0.39.1) --- CMakeLists.txt | 2 +- docs/README.md | 1 + docs/funstx.md | 63 ++++++++++ src/funstx.c | 336 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 docs/funstx.md create mode 100644 src/funstx.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c71943..18d46f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.39.0 LANGUAGES C) +project(fun VERSION 0.39.1 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/docs/README.md b/docs/README.md index e3d4ed8..e5814dd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ This file serves as an index of the documents in this directory. Links are relat - [build.md](./build.md) - How to build Fun with CMake, available targets, and build options (FUN_DEBUG, FUN_USE_MUSL, FUN_WITH_CPP, FUN_WITH_RUST, FUN_WITH_OPENSSL, FUN_WITH_LIBRESSL). - [cli.md](./cli.md) - Command-line usage of the `fun` executable: synopsis, options, exit codes, includes and library paths. +- [funstx.md](./funstx.md) - Syntax checker for .fun files with optional --fix auto-corrections; usage, exit codes, and limitations. - [contributing.md](./contributing.md) - How to contribute: project structure, coding style, running tests, and PR guidelines. - [style-guide.md](./style-guide.md) - Coding conventions for C and Fun (indentation, naming, idioms). - [stdlib.md](./stdlib.md) - Overview of the standard library modules under ./lib with one-line summaries. diff --git a/docs/funstx.md b/docs/funstx.md new file mode 100644 index 0000000..fe01a52 --- /dev/null +++ b/docs/funstx.md @@ -0,0 +1,63 @@ +# funstx — Fun syntax checker and fixer + +funstx is a small command‑line tool that parses .fun source files to verify syntax without executing them. It lives alongside the fun executable in the build directory and is installed by default. + +## Features +- Fast syntax checking for one or many .fun files +- Non‑executing: only parses, never runs code +- Clear error messages with file:line:col +- Optional automatic fixes with `--fix` for common formatting/token issues +- Suitable for batch/CI usage via exit codes + +## Usage + +``` +funstx [--fix] [file2.fun ...] +``` + +- Provide one or more .fun files to check. +- Add `--fix` to attempt safe, automatic corrections before re‑checking. + +### Examples +- Check a single file: + - `funstx examples/arrays.fun` +- Check and auto‑fix a file: + - `funstx --fix examples/arrays.fun` +- Bulk check and auto‑fix all examples (recursive): + - `find examples -type f -name '*.fun' -print0 | xargs -0 -n 50 funstx --fix` + +## Output +- On success: prints `path/to/file.fun: OK` +- On failure: prints `path/to/file.fun:line:col: syntax error: ` + +## Exit codes +- `0`: All provided files parsed successfully +- `1`: At least one file failed to parse (after optional fixing) +- `2`: Incorrect usage (e.g., no files provided) + +## What `--fix` does +The fixer makes conservative, idempotent edits that align files with parser expectations. It only writes changes if the fixed version parses successfully. + +Applied rules: +- Normalize line endings: CRLF/CR → LF +- Convert leading tabs to spaces; normalize indentation to multiples of two spaces +- Trim trailing spaces +- Ensure the file ends with a single newline +- Normalize type aliases: `sint8|sint16|sint32|sint64` → `int8|int16|int32|int64` (word‑boundary aware) + +Notes: +- Structural issues that require semantic changes aren’t auto‑fixed (e.g., exceeding global limits, missing required delimiters, incomplete statements). +- If parsing still fails after fixing, the original file is left unchanged and an error is reported. + +## Build and install +- Built as a regular CMake executable target `funstx` and placed in the same build directory as `fun` (e.g., `build_debug/` or `build_release/`). +- Installed by default alongside `fun` into `/usr/bin` via `make install`/`cmake --build --target install`. + +## Integration tips +- Use `funstx --fix` in pre‑commit hooks or CI to enforce consistent formatting. +- For quiet CI logs, capture stdout and only surface stderr on failure. +- Combine with `find`/`xargs` to process large trees efficiently (see examples above). + +## Limitations +- Does not execute bytecode or validate runtime behavior. +- The auto‑fixer focuses on formatting and a small set of safe token normalizations; it won’t rewrite program structure. diff --git a/src/funstx.c b/src/funstx.c new file mode 100644 index 0000000..aedef9b --- /dev/null +++ b/src/funstx.c @@ -0,0 +1,336 @@ +/** + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +#include +#include +#include +#include + +#include "bytecode.h" +#include "parser.h" + +static void usage(const char *prog) { + fprintf(stderr, "Usage: %s [--fix] [file2.fun ...]\n", prog); +} + +/* Read entire file into a malloc'd buffer terminated with '\0'. Returns 1 on success. */ +static int read_all(const char *path, char **out_buf, size_t *out_len) { + *out_buf = NULL; + *out_len = 0; + FILE *f = fopen(path, "rb"); + if (!f) return 0; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return 0; + } + long sz = ftell(f); + if (sz < 0) { + fclose(f); + return 0; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return 0; + } + char *buf = (char *)malloc((size_t)sz + 1); + if (!buf) { + fclose(f); + return 0; + } + size_t rd = fread(buf, 1, (size_t)sz, f); + fclose(f); + buf[rd] = '\0'; + *out_buf = buf; + *out_len = rd; + return 1; +} + +/* Write buffer to file atomically-ish (overwrite). */ +static int write_all(const char *path, const char *buf, size_t len) { + FILE *f = fopen(path, "wb"); + if (!f) return 0; + size_t wr = fwrite(buf, 1, len, f); + int ok = (wr == len); + fclose(f); + return ok; +} + +/* Check if c is a word constituent (identifier char) */ +static int is_word(int c) { + return (c == '_' || isalnum(c)); +} + +/* Apply auto-fixes to the given source text. Returns newly malloc'd buffer and new length. + * Idempotent, focuses on parser-related constraints: 2-space indents, no tab indents, + * CRLF->LF, trim trailing spaces, ensure final newline, normalize 'sint*' to 'int*'. + */ +static char *apply_fixes(const char *src, size_t len, size_t *out_len) { + /* First pass: normalize line endings to LF and compute an upper bound size */ + /* We'll build into a dynamic buffer that grows if needed. */ + size_t cap = len + 32; + char *out = (char *)malloc(cap); + if (!out) return NULL; + size_t o = 0; + + size_t i = 0; + while (i < len) { + /* Grow if needed */ + if (o + 16 >= cap) { + cap = cap * 2 + 64; + char *n = (char *)realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + + /* Start of line: handle indentation fixes */ + size_t line_start_out = o; + int at_bol = 1; + int spaces = 0; + + /* Process indentation: convert tabs to two spaces and count spaces */ + while (i < len) { + char c = src[i]; + if (c == '\r' || c == '\n') break; /* empty line */ + if (at_bol && (c == ' ' || c == '\t')) { + if (c == ' ') { + spaces++; + } else if (c == '\t') { + spaces += 2; + } /* replace leading tab with 2 spaces */ + i++; + continue; + } + break; + } + /* Emit normalized indentation: nearest even multiple (round up if odd) */ + if (at_bol) { + int normalized = (spaces % 2 == 0) ? spaces : spaces + 1; + for (int k = 0; k < normalized; k++) { + if (o + 1 >= cap) { + cap = cap * 2 + 64; + char *n = realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + out[o++] = ' '; + } + at_bol = 0; + } + + /* Copy rest of line, performing CRLF->LF, trimming trailing spaces later, and identifier normalization */ + size_t line_content_start = i; + size_t line_end_i = i; + int saw_cr = 0; + while (line_end_i < len) { + char c = src[line_end_i]; + if (c == '\r') { + saw_cr = 1; + break; + } + if (c == '\n') break; + line_end_i++; + } + /* Now we have [i, line_end_i) content. We'll trim trailing spaces. */ + size_t trim_end = line_end_i; + while (trim_end > i && src[trim_end - 1] == ' ') + trim_end--; + + /* Identifier normalization within the line content: replace sint* -> int* when at word boundaries */ + size_t p = i; + while (p < trim_end) { + /* Patterns to check */ + const char *keys[] = {"sint8", "sint16", "sint32", "sint64"}; + const char *vals[] = {"int8", "int16", "int32", "int64"}; + int replaced = 0; + for (int idx = 0; idx < 4; idx++) { + const char *k = keys[idx]; + size_t klen = strlen(k); + if (p + klen <= trim_end && strncmp(&src[p], k, klen) == 0) { + int left_ok = (p == i) || !is_word((unsigned char)src[p - 1]); + int right_ok = (p + klen == trim_end) || !is_word((unsigned char)src[p + klen]); + if (left_ok && right_ok) { + const char *v = vals[idx]; + size_t vlen = strlen(v); + if (o + vlen + 16 >= cap) { + cap = cap * 2 + 64; + char *n = realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + memcpy(&out[o], v, vlen); + o += vlen; + p += klen; + replaced = 1; + break; + } + } + } + if (replaced) continue; + if (o + 1 + 16 >= cap) { + cap = cap * 2 + 64; + char *n = realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + out[o++] = src[p++]; + } + + /* EOL: write single LF if we saw any EOL marker, else we'll add at the very end */ + if (line_end_i < len) { + /* consume CRLF or CR */ + if (src[line_end_i] == '\r') { + line_end_i++; + if (line_end_i < len && src[line_end_i] == '\n') line_end_i++; + } else if (src[line_end_i] == '\n') { + line_end_i++; + } + if (o + 1 >= cap) { + cap = cap * 2 + 64; + char *n = realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + out[o++] = '\n'; + } + + i = line_end_i; + (void)line_start_out; + (void)line_content_start; + (void)saw_cr; + } + + /* Ensure file ends with a single LF */ + if (o == 0 || out[o - 1] != '\n') { + if (o + 1 >= cap) { + cap = cap + 8; + char *n = (char *)realloc(out, cap); + if (!n) { + free(out); + return NULL; + } + out = n; + } + out[o++] = '\n'; + } + + *out_len = o; + /* shrink to fit */ + char *shr = (char *)realloc(out, o + 1); + if (shr) out = shr; + out[o] = '\0'; + return out; +} + +int main(int argc, char **argv) { + int do_fix = 0; + int first_file_arg = 1; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--fix") == 0) { + do_fix = 1; + first_file_arg++; + } else + break; + } + + if (argc < 1 + (do_fix ? 1 : 0) + 1) { + usage(argv[0]); + return 2; + } + + int hadError = 0; + + for (int i = first_file_arg; i < argc; i++) { + const char *path = argv[i]; + + if (do_fix) { + /* Read, fix, validate, and write back if parse succeeds */ + char *orig = NULL; + size_t orig_len = 0; + if (!read_all(path, &orig, &orig_len)) { + fprintf(stderr, "%s: cannot read file\n", path); + hadError = 1; + continue; + } + size_t fixed_len = 0; + char *fixed = apply_fixes(orig, orig_len, &fixed_len); + if (!fixed) { + fprintf(stderr, "%s: out of memory while fixing\n", path); + free(orig); + hadError = 1; + continue; + } + + /* Validate by parsing the fixed source using parse_string_to_bytecode */ + Bytecode *test_bc = parse_string_to_bytecode(fixed); + if (!test_bc) { + char msg[512]; + int line = 0, col = 0; + if (parser_last_error(msg, (unsigned long)sizeof msg, &line, &col)) { + fprintf(stderr, "%s:%d:%d: syntax error after --fix attempt: %s\n", path, line, col, msg); + } else { + fprintf(stderr, "%s: syntax error (unknown) after --fix attempt\n", path); + } + hadError = 1; + free(orig); + free(fixed); + continue; + } + bytecode_free(test_bc); + + /* If content differs or original did not end LF, write back */ + int changed = 0; + if (orig_len != fixed_len || memcmp(orig, fixed, (orig_len < fixed_len ? orig_len : fixed_len)) != 0) changed = 1; + if (changed) { + if (!write_all(path, fixed, fixed_len)) { + fprintf(stderr, "%s: failed to write fixed file\n", path); + hadError = 1; + free(orig); + free(fixed); + continue; + } + } + free(orig); + free(fixed); + } + + /* Finally, run the normal syntax check on the (possibly fixed) file */ + Bytecode *bc = parse_file_to_bytecode(path); + if (!bc) { + char msg[512]; + int line = 0, col = 0; + if (parser_last_error(msg, (unsigned long)sizeof msg, &line, &col)) { + fprintf(stderr, "%s:%d:%d: syntax error: %s\n", path, line, col, msg); + } else { + fprintf(stderr, "%s: syntax error (unknown)\n", path); + } + hadError = 1; + continue; // keep checking remaining files + } + + fprintf(stdout, "%s: OK\n", path); + bytecode_free(bc); + } + + return hadError ? 1 : 0; +}