I wrote a string type
mcyoung.xyz
mcyoung.xyz
https://docs.google.com/document/d/1o-MJPAddpfBfDZCkIHNKbMiM...
Looking at the reference[0] given at the very end of the document, wouldn't the word "sometimes" be more appropriate?
[0]: https://mrale.ph/blog/2016/11/23/making-less-dart-faster.htm...
*Saying this mostly with the intent of being proved wrong ;) kxcd://386
In my interpreting parser [1] I use a hexa hash tree [2] for storing identifiers. It is not very memory efficient, but very fast. It turns every string (from the input buffer) into a unique pointer for that string pointing to a copy of the string. In this way comparing string (identifiers) is equivalent to comparing pointers.
The idea of the hexa hash tree is that is a tree where each node has sixteen child nodes. Which node is selected is based on a step wise evaluated hash function that first takes the lower four bytes of successive characters in the string, and after reaching the end of the string, the higher four bytes of the characters. The nodes often taken up more memory space than the strings themselves.
[1] https://github.com/FransFaase/IParse/
[2] https://github.com/FransFaase/IParse/blob/master/software/Id...
But what about data logistics and CPU mechanics?
How good are we at expressing things like memory fragmentation, cache locality, throughput and latency of operations etc? All of these things affect how the machine operates in very real ways, to a degree that often trumps the higher level mathematical concepts above.
I only ever see these things explained in high-level, heuristic terms, observed as a black box or spelled out literally as code.
Is there a language based on how the CPU and memory work that I'm not aware of?
For that reason, I think that this route is not often taken. I guess that whenever modeling of execution is performed it is done to analyse the behaviour of an existing implementation in order to find ways to improve it.
If there is someone who has made an attempt to do something in this direction, I think it is Donald E. Knuth in his 'The Art of Computer Programming'. I think, for example about section 5.4 'External sorting' in Volume 3 'Sorting and Searching' [1].
[1] https://seriouscomputerist.atariverse.com/media/pdf/book/Art...
Using an integer to identify strings is very common in parsers - it's called "string interning".
A pet peeve is that benchmark suites often favor measuring performance with fibonacci or mandelbrot-type things, but string/graph/hash table workloads are much different, in multiple dimensions
Compilers and network servers are more like the latter
I still hope that other browsers will eventually implement element(), along with cross-fade() which is also in the CSS Images Module Level 4 draft <https://w3c.github.io/csswg-drafts/css-images-4/>.
Given how few of us Firefox users there are out there, I'm sadly not surprised.
if you hadn't commented that, I wouldn't have known
Just do it in the standard way, and not concern yourself with hardware implementation details. RISC-V assembly language even has standardized pseudoinstructions to do just that.
Some implementations will see the multiple opcodes as one instructions. Others won't. It's not for the programmer to worry about.
A 32-bit load still requires 2 instructions (The purpose of the lui instruction is to make this efficient). A 64-bit load requires loading 2 32-bit values, shifting one 32-bits right then performing a bitwise or. Alternatively you can hold the 64-bit value in .rodata and use a single instruction to load it.
The point is, it takes several more instruction bits and/or cycles than a `shr 2; sar 2` for clearing the two MSBs, so the optimisation given for x86 for this sequence is not useful here and the trivial solution is better.
A solution that may be better elsewhere is `btr 63; btr 62`. Since RV can specify a different destination operand you don't need an extra mov instruction as the x86 version would require.
I can't think of any others like that, but newline in different OS's get represented in various different ways (like DOS's famous CRLF, which adds an 0x0dh to the recipe). Operating systems that use 0x0ah alone could very well be the only example.
Also, maybe it's not a literal character for character replacement. Since lengths are probably changing, your pre-processed input might be shorter (or longer if you truly need to expand some escaped chars (and you have a program that just wants to break your compiler by abusing that escape sequence :P))
The article is talking about how to handle escaped characters in the source, like literal `\n` you can see on `let hi = "Hello\nWorld\nfrom my compiler!"` using your average text editor (0x5c+0x6e in ASCII), and how to build an efficient string type that mostly borrows views from the in-memory version of the source file (which could be a pre-processed version of the raw file if you hate odd line breaks).
Almost everybody... https://en.wikipedia.org/wiki/Newline#Representation mentions a couple that apparently used LFCR.
> no escape sequence is shorter than the text it represents
The idea is that you unescape immediately, and if necessary shuffle the remainder back, leaving a blank space at the end, but able to reuse the initial allocation.
Also, I don’t even know of anything that actually turns a \n into {CARRIAGE RETURN, LINE FEED}; if you want that, you’ll need \r\n. (A very few situations are a bit fuzzy about it, e.g. \r and \n are a bit wonky in Vim’s :substitute, but that’s all I can think of).
I do inline replacement of escaped values all the time, because the sequence is always longer than the replacement.
And no, you can't re-escape your unescaped text, as you can't do so losslessly in all cases. For some, the unescaped text might not obviously need re-escaping (e.g. the original text had a unicode escape for an emoji; you'll "forget" that after you unescape it).
Only if you store all of the tokens before starting the parsing, which would arguably be the bigger fish to fry in terms of conserving memory.
But it actually slows down the lexer: by an insignificant amount if the lexer is the traditionally written one, or by quite a lot if it's one of those new-fangled ones that use SIMD-instructions speed up finding the token boundaries... but such lexers also benefit wrom the ability to re-use unneeded parts of the source input (like quotes or spaces) for storing auxillary data.
I’ve done this in a parser where I wanted to avoid storing line and column numbers instead of a single byte offset, but still wanted to provide the more user-friendly line and column numbers in the error message.
In an interpreter there may be many string constants that aren't used (e.g. docstrings in Python).
And probably in a compiler too, since you may import a ton of library code and only use 1 function.
Storing escapes as tokens also lets you point to them in warnings. I think most compilers warn about bad octal escapes like \777 these days.
Pushing that part of the lexer's task so late on the off-chance it probably won't be needed IMHO only makes sense if you're writing a parser for an IDE/LSP in which case you don't need to unescape the string at all.
On the other hand, the principle of validating your input up front suggests that you should at least recognize the escapes, if not store their decoded from.
That is, you can be lazy by only parsing \\ and \" to find the closing quote, and producing 1 token. But that's nearly the same thing as doing a full-decoding. The downsides are that it allows invalid input deeper into the compiler, and it doesn't alert the user to errors as quickly as possible.
most of the string constants do actually see use in the program
[citation needed] :) I don't see how that can possibly hold across languages (Go, Java, Swift, etc.), and I'm not even sure that's true for C/C++.
only makes sense if you're writing a parser for an IDE/LSP in which case you don't need to unescape the string at all.
Clang's front end is used both for LSP and for code gen, so that distinction doesn't apply in all cases. It seems like most projects want to reuse their front ends these days.
And again the LSP will want to warn about invalid escapes like \777 and \u{9999999}.
----
EDIT: Although I guess in C, emitting all the errors up front, and then storing the decoded form in place, is probably about the same cost. If there's no allocation, then I might treat eager decoding as "free".
I think the problem is that you lose info, and most languages have more structure in their string literals, e.g. Swift has \(var) interpolation, and IDEs in Rust want to do things with {} format! strings, etc.
* Have some of your strings be guaranteed-memoized, then just pass an index into the global/thread-local array of memoized strings. * In tight loops working on a few strings, ensure they are all short, then forego pointers altogether and just use the values.
etc.
It sounds like you're just hoping for either a dynamically typed language (eg. Python) or for a language which is fully type-inferred (eg. Haskell). Which way is it?
The only language with zero restrictions relative to the interpreter is the language that interpreter acts on, e.g. aarch64 machine code for a specific processor instance. And even then, the restrictions of that CPU are very much present.
What do you have in mind for an unrestricted language?