Files
five/compiler/pp/std.ch
CharlesKWON 5b1d3fb32f feat(pp,rtl): pre-release accuracy round (Wave 3)
Four audit findings around correctness/consistency in std.ch and the
SORT/UPDATE/TOTAL handlers:

  * #13: TOTAL/UPDATE key idiom inconsistency documented as inherent.
    TOTAL evaluates `<key>` only in the source workarea so verbatim
    `<{key}>` (alias-qualified or `_FIELD->`-prefixed by the user)
    works. UPDATE evaluates the same block in BOTH master and detail
    context, so it must wrap as `_FIELD-><key>` to dispatch to
    whichever WA is selected at eval time. The two rules look alike
    but their evaluation contexts differ — also documented in
    std.ch alongside both rules so the asymmetry isn't a surprise.
    Plus: TOTAL TO and ON are now mandatory (matching the COUNT/
    UPDATE pattern from Wave 1) — bare TOTAL would have produced
    broken syntax via the unconditional `<(f)>`/`<{key}>` template
    references.

  * #15/#16: SDF / DELIMITED variants of COPY and TO PRINTER /
    TO FILE variants of LIST / DISPLAY are now matched by stub
    rules (placed *before* the regular rules so they win) that
    expand to a new `__dbNotImpl(reason)` RTL primitive raising a
    clear `&hbrt.HbError`. BEGIN SEQUENCE / RECOVER catches the
    panic, so callers get a real error instead of the previous
    silent dispatch-to-regular-DBF-copy.

  * #19: SORT /C (case-insensitive) now actually folds case before
    the string compare, instead of being silently treated as
    ascending. Suffix parser also rebuilt as a multi-letter scanner
    so `name/CD`, `name/DC`, `name/C/D`, `name/D/C` all parse the
    same way — combine /C and /D freely. Unknown suffix letters
    (e.g., `name/X`) leave the suffix attached to the field name
    so a stray slash in user input doesn't get silently mangled
    into a broken field reference.

  * #27 SET DELETED: verified with a regression test that
    `SET DELETED ON` causes COUNT/COPY (and by extension
    SORT/TOTAL/JOIN/UPDATE — all of which iterate via Area.Skip)
    to skip rows marked deleted. The filtering is implemented at
    the workarea level (skipFilter in dbf.go honors hbrdd.IsSetDeleted)
    so no RTL changes were needed; this commit just adds the
    coverage so the behavior doesn't silently regress.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:01:42 +09:00

190 lines
9.0 KiB
Plaintext

/*
* std.ch — Five standard preprocessor rules
*
* Equivalent to harbour-core/include/std.ch. Translates xBase legacy
* commands into function calls so the parser does not have to know
* about them. Auto-loaded by compiler/pp at startup.
*
* Phase A: only rules whose backend RTL function already exists in
* Five. Rules whose backend is not yet implemented (COPY, SORT,
* COUNT, SUM, AVERAGE, TOTAL, JOIN, LIST, DISPLAY, LABEL, REPORT,
* DIR) are deliberately NOT included here — the parser still handles
* them as silent no-ops until their RTL backend lands.
*
* Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
* All rights reserved.
*/
/* --- file system --- */
#command ERASE <(f)> => FErase(<(f)>)
#command DELETE FILE <(f)> => FErase(<(f)>)
#command RENAME <(s)> TO <(d)> => FRename(<(s)>, <(d)>)
/* --- workarea lifecycle ---
Order matters: literal-keyword forms first, then bare CLOSE,
then the alias-form last so it doesn't shadow the others. */
#command CLOSE ALL => DbCloseAll()
#command CLOSE DATABASES => DbCloseAll()
#command CLOSE => DbCloseArea()
#command CLOSE <a> => <a>->( DbCloseArea() )
/* --- record state --- */
#command COMMIT => DbCommit()
#command UNLOCK ALL => DbUnlock()
#command UNLOCK => DbRUnlock()
/* --- record search --- */
#command LOCATE [FOR <for>] [WHILE <while>] ;
[NEXT <next>] [RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbLocate(<{for}>, <{while}>, <next>, <rec>, <.rest.>)
#command CONTINUE => __dbContinue()
/* --- analytical (no extra RTL — just dbEval) ---
These mirror Harbour's std.ch but use single-value forms. Multi-
expression SUM/AVERAGE (`SUM x, y TO sx, sy`) use optional-repeat
syntax in Harbour and can be added here once a real test exercises
the more elaborate form. */
/* COUNT/SUM/AVERAGE require TO <var> — without it the rewrite
would produce naked assignment with no LHS. Match Harbour
std.ch which also makes TO non-optional. */
#command COUNT TO <v> [FOR <for>] [WHILE <while>] ;
[NEXT <next>] [RECORD <rec>] [<rest:REST>] [ALL] => ;
<v> := 0 ; dbEval( {|| <v> := <v> + 1 }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* SUM and AVERAGE accept multiple paired expressions/destinations:
`SUM x, y, z TO sx, sy, sz`. The optional `[, <xN>]` and
`[, <vN>]` repeats are matched pairwise; the result template's
chained `<v1> :=[ <vN> :=] 0` and comma-list inside the dbEval
block expand once per extra pair. Single-pair usage is unchanged. */
#command SUM <x1> [, <xN>] TO <v1> [, <vN>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
<v1> :=[ <vN> :=] 0 ; ;
dbEval( {|| <v1> := <v1> + <x1>[, <vN> := <vN> + <xN>] }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
#command AVERAGE <x> TO <v> ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
<v> := __dbAverage( <{x}>, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* --- bulk record export ---
COPY TO copies visible records of the current workarea into a fresh
DBF. FIELDS/FOR/WHILE/NEXT/RECORD/REST work as in Harbour. SDF and
DELIMITED variants are not implemented; the matching rules below
raise a clear runtime error so callers don't quietly get a regular
DBF copy when they asked for an SDF dump. Order matters: the SDF /
DELIMITED rules must come before the regular COPY rule. */
#command COPY [TO <(f)>] [FIELDS <fields,...>] SDF [<*tail*>] => ;
__dbNotImpl("COPY TO ... SDF")
#command COPY [TO <(f)>] [FIELDS <fields,...>] DELIMITED [<*tail*>] => ;
__dbNotImpl("COPY TO ... DELIMITED")
#command COPY [TO <(f)>] [FIELDS <fields,...>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbCopy( <(f)>, { <(fields)> }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* SORT TO copies the visible records into a fresh DBF in key order.
Each key in `<fields>` may carry `/D` for descending; default is
ascending. */
#command SORT [TO <(f)>] [ON <fields,...>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbSort( <(f)>, { <(fields)> }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* --- console output ---
LIST emits every record matching the filter; DISPLAY without ALL
shows just the current record. Both share __dbList — lAll
distinguishes them. TO PRINTER / TO FILE redirection is not yet
implemented; the stub rules below surface a clear error rather
than silently sending output to stdout when a printer/file was
requested. Order matters: more specific rules first. */
#command LIST [<v,...>] TO PRINTER [<*tail*>] => ;
__dbNotImpl("LIST ... TO PRINTER")
#command LIST [<v,...>] TO FILE <(f)> [<*tail*>] => ;
__dbNotImpl("LIST ... TO FILE")
#command DISPLAY [<v,...>] TO PRINTER [<*tail*>] => ;
__dbNotImpl("DISPLAY ... TO PRINTER")
#command DISPLAY [<v,...>] TO FILE <(f)> [<*tail*>] => ;
__dbNotImpl("DISPLAY ... TO FILE")
#command LIST [<v,...>] [<off:OFF>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbList( <.off.>, { <{v}> }, .T., ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
#command DISPLAY [<v,...>] [<off:OFF>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [<all:ALL>] => ;
__dbList( <.off.>, { <{v}> }, <.all.>, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* TOTAL TO writes one record per consecutive run of equal key values
from the source. Numeric fields named in FIELDS are summed; every
other (non-memo) field takes the first record's value. The source
must already be sorted/indexed on the key for the grouping to
produce one row per distinct value.
Note on key syntax — TOTAL evaluates `<key>` only in the source
workarea, so `<{key}>` (verbatim blockify) is enough; user can
write `ON src->dept` (alias-qualified) or `ON _FIELD->dept`
(current-area). UPDATE FROM evaluates the key block in BOTH
master and detail context and therefore needs `_FIELD->`-wrapped
bare keys instead — the two rules look superficially similar but
their evaluation contexts differ. */
#command TOTAL TO <(f)> ON <key> [FIELDS <fields,...>] ;
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
[RECORD <rec>] [<rest:REST>] [ALL] => ;
__dbTotal( <(f)>, <{key}>, { <(fields)> }, ;
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
/* JOIN merges the current ("master") workarea with the named
detail alias into a fresh DBF, emitting one output row per
master/detail pair where FOR evaluates true. */
#command JOIN [WITH <(alias)>] [TO <(f)>] [FIELDS <fields,...>] ;
[FOR <for>] => ;
__dbJoin( <(alias)>, <(f)>, { <(fields)> }, <{for}> )
/* UPDATE FROM walks the named detail alias and applies the
REPLACE ... WITH ... clauses to the matching master record.
Both areas should be sorted on the key for the default forward-
walk; pass RANDOM to scan master from top for each detail key.
Note 1: ON <key> is wrapped as `_FIELD-><key>` rather than the bare
`<{key}>` Harbour uses, because the same block must evaluate
against both master and detail. Bare identifiers don't auto-bind
to fields under Five — `_FIELD->` makes the dispatch explicit.
Note 2: FROM/ON/REPLACE are all required (Harbour technically allows
them in any order but every real call site provides all three). The
former optional brackets allowed compile-clean garbage like a bare
`UPDATE` to expand to a broken-syntax call. Keep them mandatory. */
#command UPDATE FROM <(alias)> ON <key> [<rand:RANDOM>] ;
REPLACE <f1> WITH <x1> [, <fN> WITH <xN>] => ;
__dbUpdate( <(alias)>, {|| _FIELD-><key> }, <.rand.>, ;
{|| _FIELD-><f1> := <x1>[, _FIELD-><fN> := <xN>] } )
/* --- bulk maintenance --- */
#command REINDEX => DbReindex()
#command PACK => DbPack()
#command ZAP => DbZap()
/* --- input / shell --- */
#command KEYBOARD <text> => Keyboard(<text>)
#command RUN <*cmd*> => hb_Run(<(cmd)>)
/* --- legacy GET system ---
MENU TO is intentionally absent: it requires the @ PROMPT statement
companion which Five doesn't implement. Adding the rule would let
user code compile and then panic at runtime on the missing
__MenuTo() symbol. Keep the parser's silent no-op for MENU TO until
@ PROMPT lands. */
#command CLEAR GETS => GetList := {}