preserveFieldNames

preserveFieldNames

Setting Name

preserveFieldNames

Location

config.js (root of a Profound.js instance)

Type

boolean

Available Since

Profound.js 7.18.0

Affects

Profound.js runtime and the Converter (single source of truth)

Runtime Impact

Yes — controls how RPG/CL identifiers containing # and @ are resolved at runtime

Overview

In RPG, the characters # and @ are valid inside variable, field, file, record-format, subroutine, procedure, and program names. JavaScript does not allow these characters in identifiers.

preserveFieldNames controls how Profound.js handles that mismatch:

Setting

Behavior

Setting

Behavior

false (legacy behavior)

The Converter rewrites # and @ to . A field named myfield@ becomes myfield everywhere it is generated and referenced. The runtime resolves the renamed identifier directly.

true (new behavior introduced in 7.18.0)

The Converter keeps the original name and emits this["..."] style accessors. The runtime resolves names through the same indirection. myfield@ stays myfield@ and is accessed as this["myfield@"].

Because the same flag drives both the Converter and the runtime, the generated JS files and the framework that runs them always agree on the symbol shape — provided the flag is not changed without reconverting (see warning below).


Default Value

Definition. The "default value" of a config setting is the value Profound.js resolves to when the setting is not present in config.js at all.

Default: false (resolves as undefined → falsy → legacy behavior).

This default exists for backward compatibility. Customer instances that were created before 7.18.0 already have JS files generated with the underscore-replacement rule. Their config.js contains no preserveFieldNames entry, the runtime treats it as falsy, and both halves remain consistent with the JS already on disk.


Behavior by Instance Type

Brand-new instances created on Profound.js 7.18.0 or later

The scaffold writes preserveFieldNames: true into the generated config.js. New instances opt into the new behavior from day one.

Existing instances upgraded to 7.18.0 or later

Upgrading the Profound.js runtime does not modify a customer's existing config.js. The setting stays absent (or stays at whatever value it had), and the runtime continues to use the legacy behavior. No customer action is required to keep working after an upgrade.


Changing preserveFieldNames on an existing instance

Reconversion required: preserveFieldNames controls the shape of the generated JS. Flipping the flag without reconverting will break the application, because the JS on disk references symbols in one shape while the runtime resolves them in the other.

If a customer changes preserveFieldNames on an existing instance, they MUST re-run the Converter against all RPG/CL source so the generated JS matches the new setting.
This applies in both directions (false → true and true → false).


Scope of the new behavior in 7.18.0

When preserveFieldNames: true, all three originally planned phases are active:

Phase

Description

Phase

Description

1

Fields and data structures

RPG/CL field definitions and their uses, including database table fields, display file fields, printer file fields, and any externally discovered fields.

2

File names and record format names

Database tables, display files, printer files. Accessed via quoted / this[] syntax (e.g. display["myscreen@"].execute()).

3

Function, subroutine, procedure, and program-derived names

Exported and invoked via this["..."] / exports["..."]


Examples

Each example shows:

Aspect

Description

Aspect

Description

1

The RPG source

Original RPG code snippet.

2

Converter output without preserveFieldNames (legacy)

Output where # and @ are rewritten to _.

3

Converter output with preserveFieldNames: true

Output where names are preserved via this[...] / exports[...].

4

Fields

Demonstrates field naming conventions.

Fields

RPG:

dcl-s @output varchar(50); @output = 'PASS'; dsply @output;

Without preserveFieldNames (legacy):

_output = 'PASS'; console.log(_output);

With preserveFieldNames: true:

this["@output"] = 'PASS'; console.log(this["@output"]);

Data structure — QUALIFIED

RPG:

Dcl-Ds @someDS QUALIFIED; @Field1 Char(10) inz('1'); #Field2 Char(10); End-Ds; if @someDS.@Field1 = '1'; output = 'PASS'; endif;

Without preserveFieldNames (legacy):

if (_someDS._Field1.rtrim() === '1') { output = 'PASS'; }

With preserveFieldNames: true:

if (this["@someDS"]["@Field1"].rtrim() === '1') { output = 'PASS'; }

Data structure — NON-QUALIFIED

RPG:

Dcl-Ds @someDS; @Field1 Char(10) inz('1'); #Field2 Char(10); End-Ds; if @Field1 = '1'; output = 'PASS'; endif;

Without preserveFieldNames (legacy):

if (_Field1.rtrim() === '1') { output = 'PASS'; }

With preserveFieldNames: true:

if (this["@Field1"].rtrim() === '1') { output = 'PASS'; }

Note — why the new behavior helps even when collisions don't break things

Under the legacy behavior, the converter prevents JS-level clashes for RPG-defined symbols by appending a numeric suffix. @FIELD1 and #FIELD1 would emit as _FIELD1 and _FIELD12 respectively — the two fields stay distinct, but the suffixed names no longer resemble the RPG source, and the suffix order can shift if you add or remove fields. With preserveFieldNames: true the names stay readable and stable as this["@FIELD1"] and this["#FIELD1"].


Record format

RPG (printer file with a record format named REC#):

dcl-f printer9p printer Usage(*Output) UsrOpn; open printer9p; move testdata rec#; write(e) DETAIL; close printer9p;

Without preserveFieldNames (legacy):

printer9p.open(); pjs.move(testdata, rec_); try { printer9p.detail.write(); } catch (err) { pjs.setError(err); } printer9p.close();

With preserveFieldNames: true:

printer9p.open(); pjs.move(testdata, this["rec#"]); try { printer9p.detail.write(); } catch (err) { pjs.setError(err); } printer9p.close();

Files (display / printer with @ or # in the file or format name)

RPG (display file MYSCRN@ with record format FMT#1):

dcl-f myscrn@ workstn; exfmt fmt#1;

Without preserveFieldNames (legacy):

// File and record-format names rewritten in-place. // In some positions (e.g. file prefixes) the legacy rule cannot // safely rename the symbol and the Converter raised an error instead. myscrn_.fmt_1.execute();

With preserveFieldNames: true:

display["myscrn@"]["fmt#1"].execute();

Functions (program entrypoint with @ / # in the program name)

RPG (program named MY@PGM):

ctl-opt main(MY@PGM); dcl-proc MY@PGM; // ... end-proc;

Without preserveFieldNames (legacy):

function my_pgm() { // ... } exports.default = my_pgm;

With preserveFieldNames: true:

function my_pgm() { // JS function identifier itself stays sanitized // ... } exports["my@pgm"] = my_pgm; // export key preserves the original program name exports.default = my_pgm;

Procedures

RPG (three procedures whose names differ only by _ / @ / # — a classic legacy collision):

DCALL_1 PR D prParm_1 2p 0 D prParm_2 2p 0 DCALL@1 PR D prParm@1 2p 0 D prParm@2 2p 0 DCALL#1 PR D prParm#1 2p 0 D prParm#2 2p 0 PROC = %Paddr(CALL#1); Callb PROC;

Without preserveFieldNames (legacy):

// CALL_1, CALL@1, CALL#1 all sanitize to call_1 — the three // distinct procedures collide into a single export. exports["call_1"] = function () { /* which one? */ }; proc = pjs.procAddr("call_1"); this[proc](pjs.refParm("parm1"), pjs.refParm("parm2"));

With preserveFieldNames: true:

exports["call_1"] = function () { /* CALL_1 */ }; exports["call@1"] = function () { /* CALL@1 */ }; exports["call#1"] = function () { /* CALL#1 */ }; proc = pjs.procAddr("call#1"); this[proc](pjs.refParm("parm1"), pjs.refParm("parm2"));

Subroutines

RPG:

ExSr New@Rate; BegSr New@Rate; rate = 50; EndSr;

Without preserveFieldNames (legacy):

new_Rate(); function new_Rate() { rate = 50; }

With preserveFieldNames: true:

this["new@Rate"](); exports["new@Rate"] = function () { rate = 50; };


CL Fields

Per the design, CL variable definitions and uses follow the same rule as RPG fields: under the legacy behavior the converter rewrites #/@ to _; with preserveFieldNames: true the original name is kept and accessed via this[…].

CL Source

DCL VAR(&MY@VAR) TYPE(*CHAR) LEN(10) VALUE('PASS') DCL VAR(&CNT#) TYPE(*DEC) LEN(3 0) CHGVAR VAR(&CNT#) VALUE(1) SNDPGMMSG MSG(&MY@VAR)

Without preserveFieldNames (legacy):

my_var = "PASS"; cnt_ = 0; cnt_ = 1; pjs.sndpgmmsg(my_var);

With preserveFieldNames: true:

this["my@var"] = "PASS"; this["cnt#"] = 0; this["cnt#"] = 1; pjs.sndpgmmsg(this["my@var"]);

SQL Host-Variable Binding

Host variables that contain # or @ are bound through the same mechanism as any other field — the difference is only in how the symbol is resolved when the binding array is constructed and when the result is fetched back.

RPG:

dcl-s @acct Char(15) inz(' 42110001111'); dcl-s actyp@ Char(2); Exec Sql Select Actyp Into :actyp@ From gelmas Where comno = 1 and gltyp = 1 and glano = :@acct; if actyp@ = 'EX'; output = 'PASS'; endif;

Without preserveFieldNames (legacy):

sql_statement1 = pjs.prepare( "Select Actyp From gelmas Where comno = 1 and gltyp = 1 and glano = ?" ); sql_statement1.bindParameters([ [acct, SQL_PARAM_INPUT] ]); sql_statement1.execute(); pjs.fetch(sql_statement1, actyp); if (actyp_ === 'EX') { output = 'PASS'; }

With preserveFieldNames: true:

sql_statement1 = pjs.prepare( "Select Actyp From gelmas Where comno = 1 and gltyp = 1 and glano = ?" ); sql_statement1.bindParameters([ [this["@acct"], SQL_PARAM_INPUT] ]); sql_statement1.execute(); pjs.fetch(sql_statement1, this["actyp@"]); if (this["actyp@"] === 'EX') { output = 'PASS'; }

The same applies to all SQL forms — EXEC SQL FETCH NEXT … INTO :hostVar, multi-row fetch into an array DS, SELECT INTO, etc. The host-variable expression is just another field reference.


Copybooks

Copybook contents are inlined at the /copy site. The same rule applies to anything declared inside the copybook — under the legacy behavior #/@ are rewritten in-place; with preserveFieldNames: true they are preserved at both the definition (inside the copybook) and at the call site (inside the including program).

RPG: (Main Program)

ctl-opt dftactgrp(*no); /copy specchars_copy exsr do@calc; dsply result; *InLr = *On;

RPG: (specchars_copy Copybook)

dcl-s @result packed(7:0); begsr do@calc; @result = pow(2 : 8); result = @result; endsr;

Without preserveFieldNames (legacy):

function spec_pgm() { pjs.import("./*.js"); do_calc(); console.log(result); flags["LR"] = true; return result; pjs.include("specchars_copy.js"); } exports.default = spec_pgm; exports["do_calc"] = function () { result = Math.pow(2, 8); // collides with any other field named result result = _result; };

With preserveFieldNames: true:

function spec_pgm() { pjs.import("./*.js"); this["do@calc"]; console.log(result); flags["LR"] = true; return result; pjs.include("specchars_copy.js"); } exports.default = spec_pgm; exports["do@calc"] = function () { this["@result"] = Math.pow(2, 8); result = this["@result"]; };

Prefix-Derived Field Names

When an externally described file is declared with prefix(@) or prefix(#), every field in the record format gets that character prepended. Under the legacy behavior the prefix character was sanitized — and in some prefix positions the rename could not be performed safely and the Converter raised an error. With preserveFieldNames: true the prefix is preserved and the prefixed fields are accessed through this[…].

RPG:

dcl-f productsp keyed prefix(@); dcl-s data varchar(50); Chain (111) productsp; If %Found(productsp); data = %Char(@PRID); EndIf; dsply data;

Without preserveFieldNames (legacy):

productsp.getRecord([111]); if (productsp.found()) { data = pjs.char(_prid); // prefix char dropped; in some prefix positions the Converter would have failed with a conversion error. } console.log(data);

With preserveFieldNames: true:

productsp.getRecord([111]); if (productsp.found()) { data = pjs.char(this["@prid"]); } console.log(data);

This category is one of the strongest reasons the new behavior exists: prefix-derived fields are the case where the legacy rule was least able to produce correct output, and where customers most often saw unavoidable conversion errors before 7.18.0.


Why the new behavior exists

  • The legacy output no longer matches the RPG source, which hurts readability and debugging.

  • Underscore replacement produced unreadable, unstable names: myfield@ and myfield# both sanitize to myfield_, and the converter then disambiguates by appending a numeric suffix (myfield_, myfield_2). The fields stay distinct, but the names no longer resemble the RPG source and can shift order if fields are added or removed.

  • Some positions (e.g. file prefix characters) cannot be replaced safely and previously produced conversion errors.

  • Every new framework integration (display files, printer files, tables, SQL) had to re-implement the rename rule. this["..."] indirection removes that burden.


Quick reference

Scenario

preserveFieldNames in config.js

Behavior

Reconversion needed?

Scenario

preserveFieldNames in config.js

Behavior

Reconversion needed?

Pre-7.18.0 customer, upgraded to 7.18.0, no edits

absent

Legacy (underscore replacement)

No

Brand-new 7.18.0+ instance

true (written by scaffold)

New (names preserved via this[])

No

Existing customer opts in

flipped from absent / false to true

New

Yes — reconvert all RPG/CL

Existing customer reverts

flipped from true to false / absent

Legacy

Yes — reconvert all RPG/CL