fluffos/testsuite/std/database.lpc
Yucong Sun fb2202546c
lpc-syntax: source-following LPC formatter with printWidth wrapping and full testsuite reformat (#1270)
Rebuilds the LPC formatter (tools/lpc-syntax/format.mjs) on top of the
existing grammar-driven tokenizer as a dependency-free engine (node
only). The supported surfaces are testsuite/format.sh (corpus CLI /
CI check) and the VS Code extension; an ESLint plugin was prototyped
mid-branch and deliberately removed again -- a thin wrapper adding an
npm dependency without using any framework capability (an LSP server
is the planned next editor-integration surface):

- Configurable line-wrap width (`options.printWidth`, default 100 --
  matching ColumnLimit in src/.clang-format) and
  indent size (`options.indentSize`, default 2): a rendered line over
  printWidth gets its outermost splittable bracket group (call args,
  array/mapping literal) broken one element per line, recursively.
  Wrap slices carry their context (mapping element vs not, pending
  ternary '?' count) so key colons stay tight and ternary colons stay
  spaced on every pass.
- Line-break decisions follow the source instead of forcing a
  canonical shape both ways: a `{ ... }` body the source wrote on one
  line stays one line whatever its statement count (and statement
  groups sharing a source line stay merged, single-spaced), if it
  still fits printWidth; a call/condition/declaration already split across
  multiple source lines keeps that layout; a genuinely empty block
  always collapses to `{}`; a brace-less if/while/for/foreach/else
  body the source wrote on its own line keeps that break too --
  including else-if chains and nested dangling-if chains, with an
  `else` re-indenting to the nearest `if` (the one it binds to). The
  tracking works inside anonymous-function bodies nested in call
  arguments (statements there get one line each; nothing glues or
  leaks indent), and `} while` only stays joined for a real do-while
  body. An empty or whitespace-only source (testsuite/clone/inh0.lpc
  is a real intentionally-empty corpus file) formats to an empty
  string, not a manufactured newline.
- NON-empty array/mapping/closure literals (`({ ... })`, `([ ... ])`,
  `(: ... :)`) get one inner padding space; EMPTY `({})`/`([])` stay
  tight -- both confirmed against the pristine, pre-formatting corpus
  rather than an already-reformatted one, since the latter is circular
  (it can only reflect what a prior, possibly-buggy pass already did).
  A mapping's key:value colon is tight before it (`([ "a": 1 ])`,
  matching new()'s class-member-initializer colon), distinguished from
  a ternary colon appearing as the mapping's value via a per-bracket-
  depth `?`/`:` counter; case/default label colons are tight via
  keyword-armed, ternary-aware detection (correct even mid-line and in
  one-lined switches). Bare "::" (the parent/efun bypass call with no
  left-hand qualifier) keeps normal spacing before it; only a
  qualified `identifier::`/`efun::` form is tight on both sides.
- A token-merge safety net in renderLine guarantees no two tokens are
  ever butted together whose concatenation re-lexes differently:
  `a - --b` must not render as `a ---b` (which re-lexes as
  `(a--) - b`), `- -x` must not become the pre-decrement `--x`, and
  `f( ::g() )` must not become `f(::g())`, whose `(:` re-lexes as a
  functional-literal opener. The check asks the real tokenizer
  (cached), so it covers present and future tight-spacing rules.
- The tokenizer handles a `#define` whose `/* */` comment opens on the
  directive line and closes on a LATER physical line (invisible
  whitespace to the directive, not a token boundary), while a QUOTE in
  a directive never extends it past its physical line (`#define Q it'`
  must not swallow the next source line; an unterminated '"' must not
  swallow everything to the next quote in the file). `\`-splices still
  continue directives, including inside a string.
- The formatter never re-spaces a macro argument that a `#define
  NAME(params) body` macro stringizes via `#param` (as opposed to
  plain `##` token paste, which operates on the value and doesn't care
  about spelling) -- `STR(1+2)` keeps stringizing to "1+2", not
  "1 + 2". Detection mirrors the driver's own preprocessing rather
  than naive text matching: directive text is analyzed after folding
  `\`-continuations and stripping comments outside quotes; an ODD-
  length '#' run stringizes (`###x` = paste-then-stringize); flags are
  unioned across every definition in the file (a dead `#if 0`
  redefinition must not strip protection -- the formatter can't
  evaluate #if truth, and over-masking only preserves spacing while
  under-masking corrupts program output); call sites accept keyword-
  shaped macro names (macros resolve before reserved words, so
  `#define string(x) #x` works); and argument boundaries mirror the
  driver's collector, which nests only the '(' character (a comma
  inside `x[...]` really splits driver arguments), with a whole-call
  verbatim freeze when a span would be bracket-unbalanced. No file in
  the corpus is excluded from stringize-aware formatting (the only
  format.sh exclusions are the two raw-byte UTF-8 fixtures).
  testsuite/single/tests/compiler/preprocessor_stringize.lpc pins
  every shape end-to-end through the real driver -- it FAILS when
  formatted with a stringize-naive formatter, so a future
  formatter regression here fails the driver suite, not just the JS
  self-checks.
- Deterministic and idempotent in all cases, including after wrapping,
  under non-default printWidth/indentSize, and across every rule
  above.

testsuite/.gitattributes marks the two deliberately-invalid-UTF8
compiler fixtures `binary`, since the repo root's `* text=auto` rule
can otherwise corrupt them on an unrelated `git checkout` (its CRLF
heuristic misfires on stray bytes inside the invalid sequences).

The full testsuite/**/*.lpc,*.c corpus is reformatted to match,
verified at every step: 0 crashes, 0 token-sequence mismatches against
the pristine pre-formatting corpus, 0 idempotency failures, the two
binary fixtures byte-identical, and the actual FluffOS driver built
and the real LPC testsuite (`driver etc/config.test -ftest`) run
against the reformatted corpus across multiple randomized-order
passes, confirming a clean `Checks succeeded.` with zero regressions.
The driver-suite step (plus an adversarial multi-agent review pass:
state-machine analysis, ~55k-input fuzzing, driver-semantics probing
of the preprocessor, and render-rule review) caught every bug class
the token-equivalence self-check is structurally blind to -- the
directive/comment tokenizer truncation, stringize arguments getting
re-spaced, adjacent-token merges, and compounding indent drift from
dangling-body bookkeeping inside call arguments.

testsuite/format.sh is the corpus auto-formatter: it formats
testsuite/**/*.lpc,*.c in place (--check verifies for CI, exit 1 when
anything is unformatted), needs only node (no npm install), hard-codes
the two malformed-UTF8 fixture exclusions alongside the .gitattributes
protection, and refuses to write any file whose output isn't
token-sequence-equivalent to the input and idempotent
(bin/format-corpus.mjs).

Spacing and layout deliberately mirror the repo's own C++ style
(src/.clang-format: Google base, IndentWidth 2, ColumnLimit 100) on
every language-common rule -- 2-space indent, 100-column wrap,
attached braces with cuddled else/while, `if (` spaced vs call-tight
parens, indented case labels, tight casts including after a keyword
(`return (string)x`), tight unary/`++`/`--`/`[]`/`->`/`::`, spaced
binary/assignment/ternary operators, directives at column 0, no
include sorting, and trailing `//` comments at least two spaces off
the code (SpacesBeforeTrailingComments: 2) with wider hand-aligned
gaps preserved exactly (AlignTrailingComments in spirit, without ever
moving a comment). LPC-specific constructs keep the corpus's own
conventions where C++ has no analogue or the corpus disagrees for a
reason: `type *name` binds the array-marker `*` to the name (~5:1
pristine, opposite of PointerAlignment: Left), literal inner padding
(`({ 1, 2 })`, `([ "a": 1 ])`) with tight empties, call-tight
`catch(`/`new(`, spaced functional-literal bounds (`(: f :)`), and
tight-before default-argument colons. clang-format's line-reflow
canonicalization is deliberately not adopted -- breaks follow the
source (see above).

The VS Code extension (tools/lpc-syntax/vscode/) gets matching
`lpc.format.printWidth`/`lpc.format.indentSize` settings and
regenerated generated-copy files (lib/tokenizer.mjs, lib/format.mjs)
sharing the exact same engine. test.mjs covers the
tokenizer/formatter/linter/generated-VS-Code-asset surface, including
regression tests pinned to every convention and bug fix above. AGENTS.md and the tools/lpc-syntax README document the
testsuite/.gitattributes gotcha, the driver-mirroring stringize
machinery, and the "validate against the real driver, not just
token-equivalence" lesson for future changes to this tooling.


Claude-Session: https://claude.ai/code/session_01HSL1G3iHXu1dd8XhnBQ2fe

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-17 11:55:21 -07:00

756 lines
13 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @brief LPMUD数据库操作模块使用链式调用优雅的增删改查
* @author 雪风@mud.ren
*/
#ifdef __PACKAGE_DB__
// 数据库配置
nosave string db_host;
nosave string db_db;
nosave string db_user;
nosave int db_type = __DEFAULT_DB__;
// 数据库查询
nosave mixed db_handle;
// 错误消息
nosave string db_error;
/**
* @brief The table which the query is targeting.
*
*/
nosave string db_table;
// 数据表头
nosave string *db_table_column = ({});
/**
* @brief SQL语句
*
* SELECT
* [DISTINCT] <select_list>
* FROM <left_table>
* [<join_type> JOIN <right_table>]
* [ON <join_condition>]
* [WHERE <where_condition>]
* [GROUP BY <group_by_list>]
* [HAVING <having_condition>]
* [ORDER BY <order_by_condition>]
* [LIMIT <limit_number>]
* [OFFSET <offset_number>]
* [UNION [ALL]]
* [SELECT ***]
*/
nosave string db_sql;
/**
* @brief The columns that should be returned.
*
*/
nosave string db_sql_columns = "*";
/**
* @brief The where constraints for the query.
*
*/
nosave string db_sql_where;
nosave string *db_sql_wheres = ({});
/**
* @brief The groupings for the query.
*
*/
nosave string db_sql_groups;
/**
* @brief The having constraints for the query.
*
*/
nosave string db_sql_havings;
/**
* @brief The orderings for the query.
*
*/
nosave string db_sql_orders;
/**
* @brief The maximum number of records to return.
*
*/
nosave int db_sql_limit;
/**
* @brief The number of records to skip.
*
*/
nosave int db_sql_offset;
// 状态和条件
nosave int db_distinct;
nosave int db_inRandomOrder;
nosave int db_withColumn;
nosave int db_autoClose = 1;
/**
* @brief 数据库连接初始化
*
* @param host
* @param db
* @param user
* @return void
*/
varargs void create(string host, string db, string user, int type) {
if (host) {
db_host = host;
}
if (db) {
db_db = db;
}
if (user) {
db_user = user;
}
if (type) {
db_type = type;
}
}
/**
* @brief 重置数据库链接配置
*
* @param db
*/
void setConnection(mapping db) {
db_host = db["host"];
db_db = db["database"];
db_user = db["user"];
if (db["type"]) {
db_type = db["type"];
}
}
/**
* @brief 是否自动关闭数据库连接
*
* @param flag
*/
void setAutoClose(int flag) {
db_autoClose = flag;
}
// 重置查询
void resetSql() {
db_error = 0;
db_withColumn = 0;
db_distinct = 0;
db_sql = "";
db_sql_columns = "*";
db_sql_where = "";
db_sql_wheres = ({});
db_sql_groups = "";
db_sql_havings = "";
db_sql_orders = "";
db_sql_limit = 0;
db_sql_offset = 0;
db_inRandomOrder = 0;
}
/**
* @brief 原生SQL调用
*
* @return this_object()
*/
object sql(string sql) {
resetSql();
db_sql = sql;
return this_object();
}
/**
* @brief 构造数据表调用
*
* @param table
* @return object
*/
object table(string table) {
resetSql();
db_table = table;
return this_object();
}
/**
* @brief 使用DISTINCT过滤重复数据
*
* @return object
*/
object distinct() {
db_distinct = 1;
return this_object();
}
/**
* @brief 针对SQL展开数组的处理
*
* @param arr 需处理的数组
* @param del 分隔符
* @return string
*/
string implodeX(mixed *arr, string del) {
string s = "";
foreach (mixed x in arr) {
// debug_message(typeof(x));
switch (typeof(x)) {
case "int":
case "float":
s += x + del;
break;
default:
s += "'" + x + "'" + del;
break;
}
}
return s[0..<sizeof(del) + 1];
}
/**
* @brief where条件查询处理
*
* @param where
* @param boolean 条件关系" AND "、" OR "...
*/
void addArrayOfWheres(mixed *where, string boolean) {
foreach (mixed *x in where) {
if (arrayp(x) && sizeof(x) == 2) {
db_sql_wheres += ({ x[0] + "='" + x[1] + "'" });
} else if (arrayp(x) && sizeof(x) == 3) {
db_sql_wheres += ({ x[0] + " " + x[1] + " '" + x[2] + "'" });
}
}
if (boolean == " OR ") {
db_sql_where += " OR " + implode(db_sql_wheres, " AND ");
} else {
db_sql_where = implode(db_sql_wheres, " AND ");
}
}
object where(mixed *x...) {
// debug_message(sprintf("%O", x));
if (sizeof(db_sql_where)) {
db_sql_where += " AND ";
}
if (arrayp(x[0])) {
addArrayOfWheres(x[0], " AND ");
} else if (sizeof(x) == 2) {
db_sql_where += x[0] + "='" + x[1] + "'";
} else if (sizeof(x) == 3) {
db_sql_where += x[0] + " " + x[1] + " '" + x[2] + "'";
}
return this_object();
}
object orWhere(mixed *x...) {
if (arrayp(x[0])) {
addArrayOfWheres(x[0], " OR ");
} else if (sizeof(x) == 2) {
db_sql_where += " OR " + x[0] + "='" + x[1] + "'";
} else if (sizeof(x) == 3) {
db_sql_where += " OR " + x[0] + " " + x[1] + " '" + x[2] + "'";
}
return this_object();
}
varargs object whereBetween(string column, mixed *x, int not) {
string between = " BETWEEN ";
if (not) {
between = " NOT BETWEEN ";
}
if (sizeof(db_sql_where)) {
db_sql_where += " AND ";
}
if (sizeof(x) == 2) {
db_sql_where += column + between + x[0] + " AND " + x[1];
}
return this_object();
}
object whereNotBetween(string column, mixed *x) {
return whereBetween(column, x, 1);
}
varargs object orWhereBetween(string column, mixed *x, int not) {
string between = " BETWEEN ";
if (not) {
between = " NOT BETWEEN ";
}
if (sizeof(x) == 2) {
db_sql_where += " OR " + column + between + x[0] + " AND " + x[1];
}
return this_object();
}
object orWhereNotBetween(string column, mixed *x) {
return orWhereBetween(column, x, 1);
}
varargs object whereNull(string column, int not) {
string null = " IS NULL";
if (not) {
null = " IS NOT NULL";
}
if (sizeof(db_sql_where)) {
db_sql_where += " AND ";
}
db_sql_where += column + null;
return this_object();
}
object whereNotNull(string column) {
return whereNull(column, 1);
}
varargs object orWhereNull(string column, int not) {
string null = " IS NULL";
if (not) {
null = " IS NOT NULL";
}
db_sql_where += " OR " + column + null;
return this_object();
}
object orWhereNotNull(string column) {
return orWhereNull(column, 1);
}
object whereIn(string column, mixed *x, int not) {
string notin = " IN ";
if (not) {
notin = " NOT IN ";
}
if (sizeof(db_sql_where)) {
db_sql_where += " AND ";
}
db_sql_where += column + notin + "(" + implodeX(x, ",") + ")";
return this_object();
}
object whereNotIn(string column, mixed *x) {
return whereIn(column, x, 1);
}
object orWhereIn(string column, mixed *x, int not) {
string notin = " IN ";
if (not) {
notin = " NOT IN ";
}
db_sql_where += " OR " + column + notin + "(" + implodeX(x, ",") + ")";
return this_object();
}
object orWhereNotIn(string column, mixed *x) {
return orWhereIn(column, x, 1);
}
/**
* @brief 分组
*
* @param column
* @return object
*/
object groupBy(string *column...) {
// todo
return this_object();
}
/**
* @brief 分组过滤
*
* @param column
* @param operator
* @param value
* @param boolean
* @return object
*/
object having(string column, string operator, mixed value, string boolean) {
// todo
return this_object();
}
/**
* @brief 排序
*
* @param column
* @param order asc / desc
* @return object
*/
varargs object orderBy(string column, string order) {
if (!nullp(column)) {
if (!stringp(order) || member_array(lower_case(order), ({ "asc", "desc" })) < 0) {
order = "ASC";
}
if (sizeof(db_sql_orders)) {
db_sql_orders += ",";
}
db_sql_orders += column + " " + upper_case(order);
}
return this_object();
}
object inRandomOrder() {
db_inRandomOrder = 1;
return this_object();
}
/**
* @brief limit处理
*
* @param n
* @return object
*/
object limit(int n) {
db_sql_limit = n;
return this_object();
}
/**
* @brief offset处理
*
* @param n
* @return object
*/
object offset(int n) {
db_sql_offset = n;
return this_object();
}
object with(string str) {
switch (str) {
case "column":
db_withColumn = 1;
break;
default:
break;
}
return this_object();
}
// 构造条件语句
private void db_sql_bindings() {
if (sizeof(db_sql_where)) {
db_sql += " WHERE " + db_sql_where;
}
if (sizeof(db_sql_orders)) {
db_sql += " ORDER BY " + db_sql_orders;
}
if (db_sql_limit) {
db_sql += " LIMIT " + db_sql_limit;
if (db_sql_offset) {
db_sql += " OFFSET " + db_sql_offset;
}
}
}
// 构造查询语句
private string db_sql() {
if (!sizeof(db_sql)) {
if (db_distinct) {
db_sql_columns = "DISTINCT " + db_sql_columns;
}
db_sql = "SELECT " + db_sql_columns + " FROM " + db_table;
db_sql_bindings();
}
return db_sql;
}
/**
* @brief 连接数据库并返回handle
*
*/
private mixed connect() {
// 连接数据库
if (!db_handle || stringp(db_handle)) {
db_handle = db_connect(db_host, db_db, db_user, db_type);
/* error */
if (stringp(db_handle))
return db_error = db_handle;
else {
// 默认mysql编码
// db_exec(db_handle, "set names utf8mb4");
}
}
return db_handle;
}
/**
* @brief 关闭数据库连接
*
*/
varargs mixed close(int flag) {
if ((flag || db_autoClose) && intp(db_handle) && db_handle && db_close(db_handle)) {
db_handle = 0;
}
return db_handle;
}
/**
* @brief 执行SQL语句并返回结果行数
*
*/
varargs mixed exec() {
mixed rows;
// 连接数据库
if (stringp(connect())) {
return db_error;
}
// 执行SQL语句
rows = db_exec(db_handle, db_sql());
/* error */
if (stringp(rows)) {
close();
return db_error = rows;
}
// 保存数据表头
db_table_column = db_fetch(db_handle, 0);
return rows;
}
varargs mixed get(string *columns...) {
mixed rows, *res;
if (sizeof(columns)) {
db_sql_columns = implode(columns, ",");
}
rows = exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
res = allocate(rows);
for (int i = 1; i <= rows; i++) {
res[i - 1] = db_fetch(db_handle, i);
}
close();
if (db_inRandomOrder) {
res = shuffle(res);
}
if (db_withColumn) {
res = ({ db_table_column }) + res;
}
return res;
}
mixed pluck(string column) {
mixed *res, *arr = ({});
int index;
db_sql_columns = column;
res = get();
/* error */
if (stringp(db_error)) {
return db_error;
}
index = member_array(column, db_table_column);
for (int i = 0; i < sizeof(res); i++) {
arr += ({ res[i][index] });
}
return arr;
}
varargs mixed first(string *columns...) {
mixed rows, *res;
int i = 1;
if (sizeof(columns)) {
db_sql_columns = implode(columns, ",");
}
rows = exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
if (db_inRandomOrder) {
i = random(rows) + 1;
}
res = db_fetch(db_handle, i);
close();
return res;
}
mixed find(int id) {
return where("id", "=", id)->first();
}
mixed value(string column) {
mixed *res = first(column);
int index = member_array(column, db_table_column);
if (sizeof(res)) {
return res[index];
}
return "";
}
/**
* @brief 数据库聚合函数
*
* @param func
* @param column
* @return private
*/
private mixed aggregate(string func, mixed column) {
mixed rows, *res;
if (column) {
db_sql = "SELECT " + func + "(" + column + ") FROM " + db_table;
db_sql_bindings();
} else {
return "";
}
rows = exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
res = db_fetch(db_handle, 1);
close();
return res[0];
}
varargs mixed count(mixed column) {
if (nullp(column)) {
column = 1;
}
return aggregate("COUNT", column);
}
mixed max(string column) {
return aggregate("MAX", column);
}
mixed min(string column) {
return aggregate("MIN", column);
}
mixed avg(string column) {
return aggregate("AVG", column);
}
mixed sum(string column) {
return aggregate("SUM", column);
}
/**
* @brief 插入
*
*/
mixed insert(mapping m) {
// 构造插入语句
db_sql = "INSERT INTO " + db_table + " (" + implode(
keys(m),
","
) + ") VALUES (" + implodeX(values(m), ",") + ")";
//执行SQL
exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
close();
return 1;
}
/**
* @brief 更新
*
*/
mixed update(mapping m) {
mixed key, value;
string sql = "";
foreach (key, value in m) {
value = typeof(value) == "string" ? "'" + value + "'" : value;
sql += key + "=" + value + ",";
}
sql = sql[0..<2];
// 构造更新语句
db_sql = "UPDATE " + db_table + " SET " + sql;
db_sql_bindings();
//执行SQL
exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
close();
return 1;
}
/**
* @brief 删除
*
*/
mixed delete() {
// 构造删除语句
db_sql = "DELETE FROM " + db_table;
db_sql_bindings();
//执行SQL
exec();
/* error */
if (stringp(db_error)) {
return db_error;
}
close();
return 1;
}
/**
* @brief 调试
*
*/
string dump() {
string line = repeat_string("-*-", 20);
return sprintf(
"\n%s\ndb_host = %s\ndb_db = %s\ndb_user = %s\ndb_handle = %d\ndb_error = %O\ndb_table = %s\ndb_table_column = %O\ndb_sql = %s\ndb_status = %s\n%s\n",
line,
db_host,
db_db,
db_user,
db_handle,
db_error,
db_table,
db_table_column,
db_sql,
db_status(),
line
);
}
#endif