|
|
@@ -283,6 +283,9 @@ VAR
|
|
|
limit in CX *)
|
|
|
TU_CmpStr : CARDINAL ; (* string comparison; two addresses on the stack,
|
|
|
signed -1/0/+1 in AX *)
|
|
|
+ TU_StrCat : CARDINAL ; (* string concatenation; dest and source addresses
|
|
|
+ on the stack, limit in CX; exit code 16 if the
|
|
|
+ result would be too long *)
|
|
|
|
|
|
abortFac : BOOLEAN ;
|
|
|
errNum : CARDINAL ; (* NOT "errNo": Compile's formal of that
|
|
|
@@ -1909,11 +1912,36 @@ BEGIN
|
|
|
Ebyte (92H)
|
|
|
END EmXchgAxDx ;
|
|
|
|
|
|
+PROCEDURE StrConcat (op : CARDINAL ; left, right : ERes ; VAR res : ERes) ; FORWARD ;
|
|
|
+PROCEDURE StrOperand (r : ERes) : BOOLEAN ; FORWARD ;
|
|
|
+
|
|
|
PROCEDURE BinOpEmit (op : CARDINAL ; left, right : ERes ; VAR res : ERes) ;
|
|
|
(* binary operation at one precedence level; folds constant operands *)
|
|
|
VAR f : LONGINT ;
|
|
|
okc : BOOLEAN ;
|
|
|
BEGIN
|
|
|
+ IF StrOperand (left) OR StrOperand (right) THEN
|
|
|
+ (* A string OPERAND - a STRING or a CHAR, which is a one-character string
|
|
|
+ for this purpose - makes this a string operation, and `+' is the only
|
|
|
+ one ISO Modula-2 defines: every other binary operator reached here
|
|
|
+ (OR, and the multiplicative and additive numeric ops) has no meaning
|
|
|
+ over a STRING, and StrConcat rejects it.
|
|
|
+
|
|
|
+ The test is StrOperand and NOT `cls = TString' because of the two
|
|
|
+ CHAR operands. `'a' + 'b'' has no STRING in it at all, so a
|
|
|
+ TString-only test let it fall through to the integer path, where the
|
|
|
+ character codes were added and CHR(195) was printed - a silently
|
|
|
+ wrong program rather than an error (see t53_strcat_type). A CHAR
|
|
|
+ operand is still a string operand, which is also what makes `a + ch'
|
|
|
+ concatenate.
|
|
|
+
|
|
|
+ Handling it here, at the head of the one routine every binary
|
|
|
+ operator funnels through, is what keeps `a + b', `writeln(a + b)',
|
|
|
+ `(a + b) = c' and `s := a + b + c' all going through the same
|
|
|
+ emission. *)
|
|
|
+ StrConcat (op, left, right, res) ;
|
|
|
+ RETURN
|
|
|
+ END ;
|
|
|
IF op = TkAnd THEN
|
|
|
IF (left.kind = 0) AND (right.kind = 0) THEN
|
|
|
res.kind := 0 ;
|
|
|
@@ -2148,6 +2176,145 @@ BEGIN
|
|
|
END
|
|
|
END EmPushStrAddr ;
|
|
|
|
|
|
+PROCEDURE NewScratchStr () : CARDINAL ;
|
|
|
+(* Allocate a fresh 256-byte global STRING and return its symbol index.
|
|
|
+
|
|
|
+ A concatenation result needs STORAGE the emitted code can name. Making the
|
|
|
+ result a string VARIABLE (kind 1) is what keeps every downstream consumer
|
|
|
+ untouched: Assignment, IoCall and ParseCmp already handle kind 1, so `s :=
|
|
|
+ a + b', `writeln(a + b)' and `(a + b) = c' work with no code of their own.
|
|
|
+
|
|
|
+ One scratch PER `+' in the source, so a repeated or nested concatenation
|
|
|
+ never shares a destination - `a + b + c' is two occurrences and gets two
|
|
|
+ scratch strings, and the first result survives until the second consumes it.
|
|
|
+ (A loop reuses its site's scratch, which is correct because each execution
|
|
|
+ writes before it reads.) Recursion would break that, but this compiler
|
|
|
+ compiles no procedures yet - see the item list.
|
|
|
+
|
|
|
+ 256 bytes is the STRING type's own size, length byte plus 255 characters:
|
|
|
+ see ParseType's TkString arm and the predefined STRING in Inittur. The name
|
|
|
+ is a placeholder (nothing looks a scratch up by name) spelled with '@', as
|
|
|
+ tmpA/tmpB are, so it cannot collide with a Modula-2 identifier. *)
|
|
|
+BEGIN
|
|
|
+ DropC (NewSym ("@@SC", KVar, TString, 256, 1, dc, 0, FALSE)) ;
|
|
|
+ dc := dc + 256 ;
|
|
|
+ RETURN symTop - 1
|
|
|
+END NewScratchStr ;
|
|
|
+
|
|
|
+PROCEDURE StrOperand (r : ERes) : BOOLEAN ;
|
|
|
+(* Is r usable as one operand of `+'? A STRING, or a CHAR - which can appear
|
|
|
+ three ways: a quoted literal (cls TScalar with chr set), a CHAR variable
|
|
|
+ (cls TChar), and a string element s[i] (cls TChar, kind 5). A number is
|
|
|
+ not, and neither is a BOOLEAN. *)
|
|
|
+BEGIN
|
|
|
+ RETURN (r.cls = TString) OR (r.cls = TChar) OR
|
|
|
+ ((r.cls = TScalar) AND r.chr)
|
|
|
+END StrOperand ;
|
|
|
+
|
|
|
+PROCEDURE EmPushStrOperand (r : ERes) ;
|
|
|
+(* Push the ADDRESS of one concatenation operand, converting a CHAR to a
|
|
|
+ length-1 string first.
|
|
|
+
|
|
|
+ A STRING operand is pushed exactly as cmpstr pushes one (EmPushStrAddr):
|
|
|
+ a variable's value already IS its address, and a literal travels inline.
|
|
|
+
|
|
|
+ A CHAR operand has no such address, so it is materialised as a
|
|
|
+ `<length 1><character>' string in a fresh 2-byte scratch and the address of
|
|
|
+ THAT is pushed. This is TPSRC9's addch/addch2, which splice a character
|
|
|
+ into a concatenation by building a one-character string. The character is
|
|
|
+ loaded into AL (LoadAtom), the character is stored at [coff+1] and the
|
|
|
+ length 1 at [coff]. At most one operand of a concatenation is a CHAR -
|
|
|
+ StrConcat rejects char+char - so at most one such scratch is ever made. *)
|
|
|
+VAR coff : CARDINAL ;
|
|
|
+BEGIN
|
|
|
+ IF r.cls = TString THEN
|
|
|
+ EmPushStrAddr (r)
|
|
|
+ ELSE
|
|
|
+ coff := dc ;
|
|
|
+ dc := dc + 2 ;
|
|
|
+ LoadAtom (r) ; (* AX := the character *)
|
|
|
+ EmStoreVar (FALSE, (coff + 1) MOD 10000H, 1) ; (* [coff+1] := AL *)
|
|
|
+ EmMovAxi (1) ; (* AX := 1 *)
|
|
|
+ EmStoreVar (FALSE, coff MOD 10000H, 1) ; (* [coff] := AL = length *)
|
|
|
+ EmPushVarAddr (FALSE, coff MOD 10000H)
|
|
|
+ END
|
|
|
+END EmPushStrOperand ;
|
|
|
+
|
|
|
+PROCEDURE StrConcat (op : CARDINAL ; left, right : ERes ; VAR res : ERes) ;
|
|
|
+(* `left + right' for strings: TPSRC2's xconcat, emitted as two runtime calls.
|
|
|
+
|
|
|
+ The layout is `<length byte><characters>' and `+' needs a place to build its
|
|
|
+ result, so this allocates one 256-byte global scratch (NewScratchStr) and
|
|
|
+ copies into it: first `dest := left' through the runtime's strstore, then
|
|
|
+ `dest := dest + right' through strcat, which appends. Using strstore for
|
|
|
+ the left operand and strcat for the right keeps the append logic in one
|
|
|
+ place and gives the compiler exactly one new entry to reach; it also means
|
|
|
+ the two calls have the same argument shape (source pushed first, destination
|
|
|
+ last, limit in CX), so the emission differs only in the entry named.
|
|
|
+
|
|
|
+ The limit is 255 - the scratch's 255 usable characters. An overflowing
|
|
|
+ result is a RUNTIME ERROR with exit code 16, TP3's `String too long', not a
|
|
|
+ truncation; see Runtime.EmitStrCat.
|
|
|
+
|
|
|
+ The operator rule: `+' concatenates, anything else over a STRING is a type
|
|
|
+ error, and char+char is ALSO a type error. ISO Modula-2 gives no meaning to
|
|
|
+ two characters summed, and inventing one (a two-character string?) would be
|
|
|
+ a guess this compiler does not make; TP3's own behaviour there is not
|
|
|
+ something the reference pins down. That char+char choice is the one place
|
|
|
+ this differs from a literal reading of the reference, and it is recorded
|
|
|
+ here and in v-TP3-STRCAT.md rather than left to be discovered.
|
|
|
+
|
|
|
+ The guard below is what ENFORCES that rule, and it only runs because
|
|
|
+ BinOpEmit's hook routes a CHAR operand here too. A hook that tested for a
|
|
|
+ STRING operand only would send `'a' + 'b'' to the integer path instead and
|
|
|
+ this test would never be reached - which is exactly what it did until
|
|
|
+ t53_strcat_type caught it. Three ways to fail it: a non-`+' operator over
|
|
|
+ a string operand, an operand that is neither a string nor a CHAR (a number),
|
|
|
+ and two CHARs with no STRING on either side. *)
|
|
|
+VAR dest : CARDINAL ;
|
|
|
+BEGIN
|
|
|
+ (* A placeholder for the paths below that bail out through Err. Err only
|
|
|
+ records the first error and does not unwind, so the caller goes on to use
|
|
|
+ res regardless: after `Err (ETypeErr)' in the guard, ParseAdd hands res
|
|
|
+ straight back to IoCall, to Assignment or to an enclosing concatenation.
|
|
|
+ The placeholder's SHAPE therefore matters. It must not claim to be a
|
|
|
+ STRING, because the string consumers reach symtab[res.idx] for any TString
|
|
|
+ (IoCall's `writeln' arm does) and idx is not a symbol here; and it must be
|
|
|
+ something LoadAtom can fold, because the numeric consumers will try. The
|
|
|
+ integer 0 is both. Leaving kind-2/TString here, as this once did, turned
|
|
|
+ `writeln('a' + 'b')' - two CHARs, a type error - into a compiler segfault
|
|
|
+ instead of an error message. *)
|
|
|
+ res.kind := 0 ;
|
|
|
+ res.imm := 0 ;
|
|
|
+ res.cls := TScalar ;
|
|
|
+ res.boff := 0 ;
|
|
|
+ res.chr := FALSE ;
|
|
|
+ IF (op # OpAdd) OR NOT StrOperand (left) OR NOT StrOperand (right) OR
|
|
|
+ ((left.cls # TString) AND (right.cls # TString)) THEN
|
|
|
+ Err (ETypeErr) ; (* a STRING on the wrong operator, a number,
|
|
|
+ or two CHARs *)
|
|
|
+ RETURN
|
|
|
+ END ;
|
|
|
+ dest := NewScratchStr () ;
|
|
|
+ (* dest := left *)
|
|
|
+ EmPushStrOperand (left) ;
|
|
|
+ EmPushVarAddr (FALSE, (symtab [dest].off) MOD 10000H) ;
|
|
|
+ EmMovCxi (255) ;
|
|
|
+ DropC (EmCall (TU_StrStore)) ;
|
|
|
+ EmAddSp (4) ; (* pop the two addresses strstore took *)
|
|
|
+ (* dest := dest + right *)
|
|
|
+ EmPushStrOperand (right) ;
|
|
|
+ EmPushVarAddr (FALSE, (symtab [dest].off) MOD 10000H) ;
|
|
|
+ EmMovCxi (255) ;
|
|
|
+ DropC (EmCall (TU_StrCat)) ;
|
|
|
+ EmAddSp (4) ;
|
|
|
+ res.kind := 1 ;
|
|
|
+ res.idx := dest ;
|
|
|
+ res.cls := TString ;
|
|
|
+ res.boff := 0 ;
|
|
|
+ res.chr := FALSE
|
|
|
+END StrConcat ;
|
|
|
+
|
|
|
PROCEDURE ParseCmp (VAR r : ERes) ;
|
|
|
(* "=" | "<>" | "<" | "<=" | ">" | ">=" *)
|
|
|
VAR op : CARDINAL ;
|
|
|
@@ -2712,6 +2879,22 @@ BEGIN
|
|
|
EXIT
|
|
|
END ;
|
|
|
ParseExpr (arg) ;
|
|
|
+ IF NOT OK () THEN
|
|
|
+ (* An argument that did not type-check has no value and arg.idx
|
|
|
+ is not a symbol. The string arm below dereferences
|
|
|
+ symtab[arg.idx] for ANY value whose class is TString - unlike
|
|
|
+ the string assign in Assignment, which can tell a failed
|
|
|
+ concat from a real one by its kind - so a `writeln' of a
|
|
|
+ rejected expression read symtab with whatever idx held
|
|
|
+ before and dereferenced it. That is exactly how
|
|
|
+ `writeln('a' + 'b')' - two CHARs, a type error since
|
|
|
+ v-TP3-STRCAT - segfaulted the compiler instead of reporting
|
|
|
+ the error. ParseSub guards its subscript the same way (see
|
|
|
+ `IF OK () THEN' before symtab[r.idx] there), and the
|
|
|
+ statement loop stops at the next iteration, so leaving the
|
|
|
+ rest of this WRITELN unparsed is harmless. *)
|
|
|
+ RETURN
|
|
|
+ END ;
|
|
|
(* One argument at a time: parse it, give it its own push or its
|
|
|
own runtime call, and only then read the next one. TPSRC8
|
|
|
pwrloop does exactly this, and it has to - a computed argument
|
|
|
@@ -3989,6 +4172,7 @@ BEGIN
|
|
|
TU_StrStore := RT_Entry (17) ; (* s := t: TPSRC8 asnvar, TPSRC2 strstore *)
|
|
|
TU_CmpStr := RT_Entry (18) ; (* string compare: TPSRC9 vcompare,
|
|
|
TPSRC2 cmpstr *)
|
|
|
+ TU_StrCat := RT_Entry (19) ; (* string concatenation: TPSRC2 xconcat *)
|
|
|
END Inittur ;
|
|
|
|
|
|
PROCEDURE HeadWord (VAR slot : CARDINAL) ;
|