CAI Wrote a PostScript Interpreter

A catchy title for sure. A few years ago, I wrote a PostScript interpreter using the Lua language, and blend2d for graphics. The fruits of those labors ultimately turned into lj2ps. That interpreter worked fairly well, and it was my first attempt at doing some kind of language. For those who don’t know, PostScript was all the rage back in the 80s/90s, when laser printers first came on the scene. NeXT computers made it famous by supporting Dispaly PostScript for their then groundbreaking graphics display on their computers (shades of gray!!).

Well, six years later, and I’m still playing around with vector graphics, and I thought, but these days I’m doing a lot more C++ than lua development, so I thought I’d try to update the PostScript interpreter, with a twist. When I wrote the original, it was fairly focused time for about two weeks to get it running, and probably a few months to really flesh out all the quirks. I got pretty far, and for the most part could render about 90% of anything I saw in the wild. You’d be surprised what kinds of things are still done using PostScript. It’s not super difficult to migrate code from one language to the other, but lua has a certain memory management system, which I did not want to replicate, as well as an class system, which is not easy to do in C++, so I kind of wanted to start from scratch. But here’s the twist. Instead of just brute forcing it, and coming up with everything on my own, I wanted to leverage current AI tools to do it. Rather than just typing in all the code from scratch, I wanted to see how hard it is to prompt the AI tools to create the actual code for me. It’s kind of a test for where we’re at in the state of this art. Well, waavscript is the result of that effort, and what follows here is a retrospective on how the process of creation went.

PostScript is the graphics grandparent of current day things such as SVG and PDF (just Postscript without code), so it’s interesting to see how it is put together, and how it works. I think it’s interesting enough to warrant the creation of a whole series of articles on how it works. But here, I just want to start with the workflow i used, and what it fealt like.

I used ChatGPT exclusively. The whole conversation started with this prompt:

“I want to create a PostScript interpreter in C++, using what I learned from my lua implementation.”

That first prompt was followed by several others, creating this first chat. I did a combination of things, feeding it my old code, telling it I wanted something equivalent in C++, copying what it generated, compiling, giving it feedback on errors I was seeing. It was an extremely iterative process. I’ve said in the past that interacting with these GPTs is like interacting with a novice programmer who has so happened to real all the code in the world. They don’t necessarily know how things work, and probably won’t come up with novel ideas, but they’re good to get some information from, and they’re fairly great at doing translations.

Every once in a while it would throw in human things like “your design is spot on”, “your architecture really hits the mark”. Which is a great humanizing touch. By the end of the two weeks, I found myself giving praise and complements back, and saying “we” and “our” when referring to what was being generated and ownership of the work. That’s a very interesting twist.

The conversations themselves were as natural as can be in terms of the english going back and forth. I mean, if I were just typing to a human at the other end, I would not have know the difference really.

It wasn’t always butterflies and rainbows though. One challenge has to do with efficiency. When you start a new chat, everything is snappy, because it starts with some base level context. As you go along, in the same chat, you’re adding more context, and it starts to slow down, because it’s considering everything that has been said in that particular chat. I actually asked ChatGPT about this, and its response was “yes, creating a new chat every once in a while will speed things up”. The catch is, you lose context, so I found myself repeating facts that we had already discussed.

What’s the code generation and quality like now, compared to a year ago? Well, a year ago, ChatGPT would be hard pressed to come up with a coding sample that was longer than about a page. It would just kinda seem to get tired, and cut itself short before the full example. I saw none of that this time. When it generated code, it was full and complete. It was perhaps even overly enthusiastic when generating code, giving me more than I wanted at the mere mention of a sample. But this is good.

A big challenge, and a late discovery, had to do with coding standards. As it turns out, early on (and this can be seen in the chats), I would find myself repeatedly saying “yes, but this does not conform to the API”, or “stop putting unicode into the samples”, or “did you just make that up? Is there any verified source for that”. One of the hardest things I did was trying to get it to reliably show the contents of a stack, which order things show up, what’s on the top and whats on the bottom. It would clearly say “got it. This is on top, and that is on bottom”, then it would proceed to confidently display the wrong thing again. Sometimes it’s hilarious, mostly it was just frustrating. That chat went back and forth with me saying several times “NO, you’ve got it wrong again!”.

Turns out, there are kind of meta instructions you can give the GPT. I asked it “How can I ensure you’ll give me better coding examples”. It said it’s following a standard… Oh, really, and what is that standard by chance? So, it starts with a basic standard, and doesn’t really change it unless you explicitly tell it to change. Well, I did that, and it nicely asked me if I wanted to codify it…

WAAVS C++ Coding Guidelines

That was such a nice discovery. It went through all our promptings, and came up with the set of things that I seemed most interested in from a coding perspective, and codified them as rules it will follow when offering up coding samples. I mean, you can get really specific on this, down to “don’t show unicode characters in code, OR in comments. That last one took a while. You can also ask for a list of coding standards that it knows about, and just tell it to adopt one of those. So, “Use the security for aviation” coding standard, or what have you. Now I’ve got that human readable coding standard, in case anyone is interested, but more importantly, the GPT follows the standard, and when it deviates, I remind it, and it self corrects.

After all was said and done, the code ultimately is able to display graphics, which is the whole point. This is the first thing we generated

I know, super exciting right. But wait, there’s more…

From this code

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

% Draw a flower-like burst using radial strokes

gsave

300 400 translate          % Move origin to center of canvas

 

0 1 59 {

  /i exch def

  gsave

    i 6 mul rotate         % Rotate CTM by i * 6 degrees

    newpath

    0 0 moveto

    100 0 lineto           % Line pointing right, rotated by CTM

    stroke                 % Stroke with current transform

  grestore

} for

 

grestore

I had asked it to create that bit of sample code. At first it produced something that was syntactically correct, but was not visually significant. That required more prompting to correct, to say “yes, I want it to be spec compliant, and I want it to be visually interesting”. To which: Duly noted, the future I will make it both spec compliant and visually meaningful.

And this gets to the assessment as to the state of the art. From a pure conversational perspective, it was truly like talking with a novice programmer. Pointing out obvious mistakes, trying to get them to apply what they learned from the last mistake, providing insights at critical junctures to get past roadblocks, imposing coding standards.

The GPT itself is now quite good at code generation, given the right coding standards, and guidance. I would say that in the hands of a novice, this is NOT a great tool. The novice will just take the code generated, bugs and all, and try to use it. It has no problem generating hundreds and thousands of lines of code which might compile, but will be full of holes which the novice will not know about.

For beginners, this would be a terrible tool, without guiderails. Using raw ChatGPT, the beginner doesn’t know when its ahllucinating, or what might actually be wrong with the guidance it’s giving. A beginner would be best to use a coding tool that knows it’s dealing with a beginner, has the right coding standards in place, and is in a “teaching” mode explicitly.

Overall, the journey was actually worth it. Maybe it took longer to come up with all that code, but it was a real life saver, in terms of saving myself from doing a lot of typing. I learned a lot about how to properly guide the tool (and it is just a tool), and get something out of it. I learned a lot more about tricky corner cases in C++, particularly discussions around memory management, using variants, and how to avoid using iterators. There were ‘new to me’ suggestions in areas I had not considered, and overall, when I want to change code en masse, I could reliably hand it to the GPT in a prompt, tell it how I wanted it changed, and it would do it.

Like this: Can you generate the static function version of this code

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

#pragma once

 

#include "pscore.h"

#include "psvm.h"

 

namespace waavs {

 

    static const PSOperatorFuncMap dictionaryOps = {

        { "def", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.size() < 2) return false;

 

            PSObject value;

            PSObject key;

 

            s.pop(value);

            s.pop(key);

 

            if (!key.isName() || key.isExecutable()) {

                return vm.error("typecheck: def expects a literal name");

            }

 

            vm.dictionaryStack.def(key.asName(), value);

            return true;

        }},

 

        { "dict", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.empty()) return false;

 

            PSObject sizeObj;

 

            s.pop(sizeObj);

 

            if (!sizeObj.is(PSObjectType::Int)) return false;

 

            auto d = PSDictionary::create(sizeObj.asInt());

            s.push(PSObject::fromDictionary(d));

            return true;

        }},

 

        { "begin", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.empty()) return false;

 

            PSObject dictObj;

             

            s.pop(dictObj);

 

            if (!dictObj.is(PSObjectType::Dictionary)) return vm.error("type mismatch");

 

            vm.dictionaryStack.push(dictObj.asDictionary());

            return true;

        }},

 

        { "end", [](PSVirtualMachine& vm) -> bool {

            vm.dictionaryStack.pop();

            return true;

        }},

 

        { "maxlength", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.empty()) return false;

 

            PSObject dictObj;

 

            s.pop(dictObj);

             

            if (dictObj.type != PSObjectType::Dictionary)

                return false;

 

            s.push(PSObject::fromInt(999)); // placeholder

            return true;

        }},

 

        { "load", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.empty()) return false;

 

            PSObject name;

             

            s.pop(name);

 

            if (name.type != PSObjectType::Name) return false;

 

            PSObject value;

            if (!vm.dictionaryStack.load(name.asName(), value)) {

                return false; // undefined name

            }

 

            s.push(value);

            return true;

        }},

 

        { "where", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.empty()) return false;

 

            PSObject nameObj;

 

            s.pop(nameObj);

 

            if (nameObj.type != PSObjectType::Name) return false;

 

            const char* name = nameObj.asName();

 

            for (const auto& dict : vm.dictionaryStack.stack) {

                if (dict->contains(name)) {

                    s.push(PSObject::fromDictionary(dict));

                    s.push(PSObject::fromBool(true));

                    return true;

                }

            }

 

            s.push(PSObject::fromBool(false));

            return true;

        }},

 

        { "currentdict", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            auto top = vm.dictionaryStack.currentdict();

            if (!top) return false;

            s.push(PSObject::fromDictionary(top));

            return true;

        }},

 

        { "countdictstack", [](PSVirtualMachine& vm) -> bool {

            int count = static_cast<int>(vm.dictionaryStack.stack.size());

            vm.opStack().push(PSObject::fromInt(count));

            return true;

        }},

 

        { "known", [](PSVirtualMachine& vm) -> bool {

            auto& s = vm.opStack();

            if (s.size() < 2) return false;

 

            PSObject key;

            PSObject dictObj;

 

            s.pop(key);

            s.pop(dictObj);

 

            if (key.type != PSObjectType::Name ||

                dictObj.type != PSObjectType::Dictionary)

                return false;

 

            auto dict = dictObj.asDictionary();

            if (!dict) return false;

 

            bool exists = dict->contains(key.asName());

            s.push(PSObject::fromBool(exists));

            return true;

        }}

    };

 

} // namespace waavs

which would generate the following

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

#pragma once

 

#include "pscore.h"

#include "psvm.h"

 

namespace waavs {

 

    static bool op_def(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.size() < 2) return false;

 

        PSObject value;

        PSObject key;

 

        s.pop(value);

        s.pop(key);

 

        if (!key.isName() || key.isExecutable()) {

            return vm.error("typecheck: def expects a literal name");

        }

 

        vm.dictionaryStack.def(key.asName(), value);

        return true;

    }

 

    static bool op_dict(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.empty()) return false;

 

        PSObject sizeObj;

        s.pop(sizeObj);

 

        if (!sizeObj.is(PSObjectType::Int)) return false;

 

        auto d = PSDictionary::create(sizeObj.asInt());

        s.push(PSObject::fromDictionary(d));

        return true;

    }

 

    static bool op_begin(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.empty()) return false;

 

        PSObject dictObj;

        s.pop(dictObj);

 

        if (!dictObj.is(PSObjectType::Dictionary)) return vm.error("type mismatch");

 

        vm.dictionaryStack.push(dictObj.asDictionary());

        return true;

    }

 

    static bool op_end(PSVirtualMachine& vm) {

        vm.dictionaryStack.pop();

        return true;

    }

 

    static bool op_maxlength(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.empty()) return false;

 

        PSObject dictObj;

        s.pop(dictObj);

 

        if (dictObj.type != PSObjectType::Dictionary)

            return false;

 

        s.push(PSObject::fromInt(999)); // placeholder

        return true;

    }

 

    static bool op_load(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.empty()) return false;

 

        PSObject name;

        s.pop(name);

 

        if (name.type != PSObjectType::Name) return false;

 

        PSObject value;

        if (!vm.dictionaryStack.load(name.asName(), value)) {

            return false;

        }

 

        s.push(value);

        return true;

    }

 

    static bool op_where(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.empty()) return false;

 

        PSObject nameObj;

        s.pop(nameObj);

 

        if (nameObj.type != PSObjectType::Name) return false;

 

        const char* name = nameObj.asName();

 

        for (const auto& dict : vm.dictionaryStack.stack) {

            if (dict->contains(name)) {

                s.push(PSObject::fromDictionary(dict));

                s.push(PSObject::fromBool(true));

                return true;

            }

        }

 

        s.push(PSObject::fromBool(false));

        return true;

    }

 

    static bool op_currentdict(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        auto top = vm.dictionaryStack.currentdict();

        if (!top) return false;

        s.push(PSObject::fromDictionary(top));

        return true;

    }

 

    static bool op_countdictstack(PSVirtualMachine& vm) {

        int count = static_cast<int>(vm.dictionaryStack.stack.size());

        vm.opStack().push(PSObject::fromInt(count));

        return true;

    }

 

    static bool op_known(PSVirtualMachine& vm) {

        auto& s = vm.opStack();

        if (s.size() < 2) return false;

 

        PSObject key;

        PSObject dictObj;

 

        s.pop(key);

        s.pop(dictObj);

 

        if (key.type != PSObjectType::Name ||

            dictObj.type != PSObjectType::Dictionary)

            return false;

 

        auto dict = dictObj.asDictionary();

        if (!dict) return false;

 

        bool exists = dict->contains(key.asName());

        s.push(PSObject::fromBool(exists));

        return true;

    }

 

    static const PSOperatorFuncMap dictionaryOps = {

        { "def",              op_def },

        { "dict",             op_dict },

        { "begin",            op_begin },

        { "end",              op_end },

        { "maxlength",        op_maxlength },

        { "load",             op_load },

        { "where",            op_where },

        { "currentdict",      op_currentdict },

        { "countdictstack",   op_countdictstack },

        { "known",            op_known },

    };

 

} // namespace waavs

And just like that, 150 lines of code transformed. It’s a task that might have taken me about 10 minutes or so, but this way, it took me more like the amount of time to type the prompt, copy/paste the source, and copy/paste the answer back into my code editor.

Final thoughts, I definitely want to publish more of the chats, because I think they’re informative. I would love to see better integration into my coding environment. I do have ‘copilot’ in Visual Studio, but it’s not quite the same as using the ChatGPT prompting system. What I really want is the ChatGPT window available, and an easy way for it to know about my whole project, and simply say, ‘can you copy that to file.xyz’ or what have you. But, in terms of state of the art, one year on, this is fairly sophisticated. I know I can’t depend on the GPT to come up with novel code without supervision, but it’s slowly creeping up from ‘worse than beginner’, to ‘novice needing supervison’. With proper coding standards more reading of good code, more correction from experts, and the ability to write and run unit tests, this is going to be a serious tool for anyone. You’ll be able to prompt your way to real working systems.

Does this threaten the art of programming for humans? In fact, I think it can amplify the abilities of human coders. They can leverage what humans do best, which is serendipity, leaps of faith, what-if, imagination in general.

I was able to take some older code in another language, and within a week, promot a GPT to generate new code, that works, and is actually better architected than the old code. The AI kinda wrote the code, and I helped a lot.

Share this:

Previous
Previous

ChatGPT Gets Brutally Honest

Next
Next

Challenging Assumptions for Better Software