Skip to content

Commit a87bd0c

Browse files
feat(qbasic): add MKI$/CVI/MKS$/CVS memory packing built-ins
Implements legacy QBasic's binary packing routines using strict x86 Little-Endian encoding (DataView API) to faithfully emulate the IEEE-754 32-bit float and 16-bit signed int layout. - tokens.js: register MKI$, MKS$, CVI, CVS as built-in identifiers - builtins.js: pack/unpack via ArrayBuffer + getCharFromCP437 bridge - tests: unit tests + Truth Vector compliance for round-trip integrity - builtins_math.json: extend ABS coverage with float formatting Co-authored-by: Gemini <218195315+gemini-cli@users.noreply.github.com>
1 parent ffca3d4 commit a87bd0c

6 files changed

Lines changed: 166 additions & 8 deletions

File tree

src/parser/qbasic/tokens.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const BuiltInTokens = {
4848
RTRIM$: 'RTRIM$', SPACE$: 'SPACE$', SPC: 'SPC', STRING$: 'STRING$',
4949
STR$: 'STR$', HEX$: 'HEX$', RIGHT$: 'RIGHT$', LEFT$: 'LEFT$', MID$: 'MID$',
5050
CHR$: 'CHR$', ASC: 'ASC', INSTR: 'INSTR', VAL: 'VAL',
51+
MKI$: 'MKI$', MKS$: 'MKS$', CVI: 'CVI', CVS: 'CVS',
5152

5253
// Mathematics
5354
INT: 'INT', FIX: 'FIX', CINT: 'CINT', RND: 'RND', SIN: 'SIN',

src/runtime/qbasic/builtins.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,57 @@ const executeVAL = (valArg) => {
9090
return parseFloat(str) || 0;
9191
};
9292

93+
/**
94+
* Emulates MKI$ (Make Integer String).
95+
* Packs a 16-bit integer into a 2-byte string using strict x86 Little-Endian order.
96+
*/
97+
const executeMKI$ = (val) => {
98+
const buffer = new ArrayBuffer(2);
99+
// true = Little-Endian encoding (Crucial for MS-DOS compatibility)
100+
new DataView(buffer).setInt16(0, Number(val) || 0, true);
101+
const bytes = new Uint8Array(buffer);
102+
return getCharFromCP437(bytes[0]) + getCharFromCP437(bytes[1]);
103+
};
104+
105+
/**
106+
* Emulates CVI (Convert to Integer).
107+
* Unpacks a 2-byte Little-Endian string back into a 16-bit signed integer.
108+
*/
109+
const executeCVI = (str) => {
110+
const s = String(str || "");
111+
const buffer = new ArrayBuffer(2);
112+
const bytes = new Uint8Array(buffer);
113+
bytes[0] = s.length > 0 ? getCP437FromChar(s.charAt(0)) : 0;
114+
bytes[1] = s.length > 1 ? getCP437FromChar(s.charAt(1)) : 0;
115+
return new DataView(buffer).getInt16(0, true);
116+
};
117+
118+
/**
119+
* Emulates MKS$ (Make Single-Precision String).
120+
* Packs an IEEE-754 32-bit float into a 4-byte string (Little-Endian).
121+
*/
122+
const executeMKS$ = (val) => {
123+
const buffer = new ArrayBuffer(4);
124+
new DataView(buffer).setFloat32(0, Number(val) || 0, true);
125+
const bytes = new Uint8Array(buffer);
126+
return getCharFromCP437(bytes[0]) + getCharFromCP437(bytes[1]) +
127+
getCharFromCP437(bytes[2]) + getCharFromCP437(bytes[3]);
128+
};
129+
130+
/**
131+
* Emulates CVS (Convert to Single).
132+
* Unpacks a 4-byte string back into a 32-bit float.
133+
*/
134+
const executeCVS = (str) => {
135+
const s = String(str || "");
136+
const buffer = new ArrayBuffer(4);
137+
const bytes = new Uint8Array(buffer);
138+
for (let i = 0; i < 4; i++) {
139+
bytes[i] = s.length > i ? getCP437FromChar(s.charAt(i)) : 0;
140+
}
141+
return new DataView(buffer).getFloat32(0, true);
142+
};
143+
93144
/**
94145
* Sysclone Native Standard Library (STDLIB).
95146
* Contains pure functions that do not require access to hardware state.
@@ -113,6 +164,10 @@ export const BuiltIns = {
113164
[BuiltInTokens.ASC]: (str) => getCP437FromChar(String(str).charAt(0) || 0),
114165
[BuiltInTokens.INSTR]: executeINSTR,
115166
[BuiltInTokens.VAL]: executeVAL,
167+
[BuiltInTokens.MKI$]: executeMKI$,
168+
[BuiltInTokens.CVI]: executeCVI,
169+
[BuiltInTokens.MKS$]: executeMKS$,
170+
[BuiltInTokens.CVS]: executeCVS,
116171

117172
// --- Mathematics ---
118173
[BuiltInTokens.INT]: (n) => Math.floor(n || 0),

src/runtime/qbasic/builtins.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { test, assertEqual, registerSuite } from '../../test_runner.js';
77
const {
88
LEN, UCASE$, LCASE$, LTRIM$, RTRIM$, SPACE$, SPC, STRING$, STR$, HEX$,
99
RIGHT$, LEFT$, MID$, CHR$, ASC, INSTR, VAL,
10+
MKI$, CVI, MKS$, CVS,
1011
INT, FIX, CINT, RND, SIN, COS, TAN, ATN, ABS, SQR, EXP, LOG
1112
} = BuiltIns;
1213

@@ -84,6 +85,24 @@ registerSuite('STDLIB: String Built-ins', () => {
8485
assertEqual(VAL("&O10"), 8, "Should parse octal numbers");
8586
});
8687

88+
test('MKI$, CVI, MKS$, CVS Memory Packing (Little-Endian IEEE/Int)', () => {
89+
// 16-bit Integer packing (258 = 0x0102)
90+
// Little-Endian means Least Significant Byte (02) comes first, then (01).
91+
const packedInt = MKI$(258);
92+
assertEqual(LEN(packedInt), 2);
93+
assertEqual(CVI(packedInt), 258);
94+
95+
// Two's complement for negatives (-1 = 0xFFFF)
96+
assertEqual(CVI(MKI$(-1)), -1);
97+
98+
// 32-bit Float packing
99+
const packedFloat = MKS$(1.5);
100+
assertEqual(LEN(packedFloat), 4);
101+
assertEqual(CVS(packedFloat), 1.5);
102+
103+
// Fallback for partial string passing
104+
assertEqual(CVI(""), 0, "Empty string should safely yield 0");
105+
});
87106
});
88107

89108
registerSuite('STDLIB: Math Built-ins', () => {

src/runtime/qbasic/compatibility.test.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@ registerSuite('Truth Vector: STDLIB: Math & Floating-Point Unit', () => {
8585

8686
test('ABS Function', () => {
8787
const code = `
88-
AbsVal = ABS(-42)
88+
AbsInt = ABS(-42)
89+
AbsFloatStr$ = LTRIM$(STR$(ABS(-3.14159)))
8990
`;
9091
const env = executeToEnv(code);
9192

92-
assertEqual(env.lookup('AbsVal'), 42, "AbsVal assertion failed");
93+
assertEqual(env.lookup('AbsInt'), 42, "AbsInt assertion failed");
94+
assertEqual(env.lookup('AbsFloatStr$'), "3.14159", "AbsFloatStr$ assertion failed");
9395
});
9496

9597
test('SQR Function', () => {
@@ -344,6 +346,34 @@ registerSuite('Truth Vector: STDLIB: Strings & Type Casting', () => {
344346
assertEqual(env.lookup('NotFoundIdx'), 0, "NotFoundIdx assertion failed");
345347
assertEqual(env.lookup('OutBoundsIdx'), 0, "OutBoundsIdx assertion failed");
346348
});
349+
350+
test('MKI$ and CVI (16-bit Integer Packing)', () => {
351+
const code = `
352+
PackedStr$ = MKI$(258)
353+
Length = LEN(PackedStr$)
354+
RestoredPos = CVI(PackedStr$)
355+
RestoredNeg = CVI(MKI$(-1))
356+
`;
357+
const env = executeToEnv(code);
358+
359+
assertEqual(env.lookup('Length'), 2, "Length assertion failed");
360+
assertEqual(env.lookup('RestoredPos'), 258, "RestoredPos assertion failed");
361+
assertEqual(env.lookup('RestoredNeg'), -1, "RestoredNeg assertion failed");
362+
});
363+
364+
test('MKS$ and CVS (32-bit Float Packing)', () => {
365+
const code = `
366+
PackedSng$ = MKS$(1.5)
367+
SngLength = LEN(PackedSng$)
368+
RestoredSng! = CVS(PackedSng$)
369+
RestoredZero! = CVS(MKS$(0))
370+
`;
371+
const env = executeToEnv(code);
372+
373+
assertEqual(env.lookup('SngLength'), 4, "SngLength assertion failed");
374+
assertEqual(env.lookup('RestoredSng!'), 1.5, "RestoredSng! assertion failed");
375+
assertEqual(env.lookup('RestoredZero!'), 0, "RestoredZero! assertion failed");
376+
});
347377
});
348378

349379
registerSuite('Truth Vector: CORE: Control Flow & Jumps', () => {

tests/truth_vectors/qbasic/builtins_math.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,23 @@
142142
{
143143
"name": "ABS Function",
144144
"syntax": "ABS(numeric_expression)",
145-
"description": "Returns the absolute value of a numeric expression.",
145+
"description": "Returns the absolute (positive) value of a numeric expression.",
146146
"example": {
147147
"code": [
148-
"PRINT ABS(-42)"
148+
"PRINT ABS(-42)",
149+
"PRINT ABS(3.14)"
149150
],
150-
"output": " 42 "
151+
"output": " 42 \n 3.14 "
151152
},
152153
"quirks_and_tests": {
153-
"description": "Standard arithmetic absolute conversion.",
154+
"description": "Validates absolute arithmetic conversion. Proves that single-precision float formatting is retained by casting the result to a string.",
154155
"setup": [
155-
"AbsVal = ABS(-42)"
156+
"AbsInt = ABS(-42)",
157+
"AbsFloatStr$ = LTRIM$(STR$(ABS(-3.14159)))"
156158
],
157159
"assertions": [
158-
{ "var": "AbsVal", "val": 42, "type": "number" }
160+
{ "var": "AbsInt", "val": 42, "type": "number" },
161+
{ "var": "AbsFloatStr$", "val": "3.14159", "type": "string", "message": "Must strictly retain the float formatting." }
159162
]
160163
}
161164
},

tests/truth_vectors/qbasic/builtins_string.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,56 @@
363363
{ "var": "OutBoundsIdx", "val": 0, "type": "number" }
364364
]
365365
}
366+
},
367+
{
368+
"name": "MKI$ and CVI (16-bit Integer Packing)",
369+
"syntax": "MKI$(integer_expression) / CVI(2_byte_string)",
370+
"description": "MKI$ converts a 16-bit signed integer into a 2-byte string using x86 Little-Endian byte order. CVI reverses this process.",
371+
"example": {
372+
"code": [
373+
"PRINT CVI(MKI$(258))"
374+
],
375+
"output": " 258 "
376+
},
377+
"quirks_and_tests": {
378+
"description": "CRITICAL QUIRK: Validates strict Little-Endian packing and two's complement for negative numbers. 258 (0x0102) must pack as CHR$(2) + CHR$(1).",
379+
"setup": [
380+
"PackedStr$ = MKI$(258)",
381+
"Length = LEN(PackedStr$)",
382+
"RestoredPos = CVI(PackedStr$)",
383+
"RestoredNeg = CVI(MKI$(-1))"
384+
],
385+
"assertions": [
386+
{ "var": "Length", "val": 2, "type": "number", "message": "MKI$ must strictly return a 2-byte string." },
387+
{ "var": "RestoredPos", "val": 258, "type": "number", "message": "CVI must perfectly restore the 16-bit integer." },
388+
{ "var": "RestoredNeg", "val": -1, "type": "number", "message": "Must respect 16-bit two's complement for negative numbers." }
389+
]
390+
}
391+
},
392+
{
393+
"name": "MKS$ and CVS (32-bit Float Packing)",
394+
"syntax": "MKS$(single_expression) / CVS(4_byte_string)",
395+
"description": "MKS$ converts a single-precision floating-point number into a 4-byte string using IEEE-754 Little-Endian format. CVS reverses this.",
396+
"example": {
397+
"code": [
398+
"PRINT CVS(MKS$(3.14))"
399+
],
400+
"output": " 3.14 "
401+
},
402+
"quirks_and_tests": {
403+
"description": "CRITICAL QUIRK: Validates 32-bit floating-point binary packing. Used heavily in demomaking to pack data structures into raw strings.",
404+
"setup": [
405+
"PackedSng$ = MKS$(1.5)",
406+
"SngLength = LEN(PackedSng$)",
407+
"RestoredSng! = CVS(PackedSng$)",
408+
"RestoredZero! = CVS(MKS$(0))"
409+
],
410+
"assertions": [
411+
{ "var": "SngLength", "val": 4, "type": "number", "message": "MKS$ must strictly return a 4-byte string." },
412+
{ "var": "RestoredSng!", "val": 1.5, "type": "number", "message": "CVS must perfectly restore the IEEE-754 single precision float." },
413+
{ "var": "RestoredZero!", "val": 0, "type": "number" }
414+
]
415+
}
366416
}
367417
]
368418
}

0 commit comments

Comments
 (0)