The world is full of Turing-complete programming languages. Just about every useful language (and most useless ones) are Turing-complete. Some even became Turing-complete by accident. Often this is great, since every Turing-complete language supports the same power of universal computation. But the power of Turing completeness also comes with a curse! The halting problem is undecidable for arbitrary programs in a Turing-complete language, and more generally, it’s impossible to analyze arbitrary programs in any non-trivial way. Sometimes we need less powerful languages.
It takes great care to design a useful language that falls short of Turing completeness. That’s the subject of this challenge!
Requirements
Write an interpreter that will accept a program and some input for the program, and produce some output. The program, input, and output are given using simple data types of your choice.
Examples of “simple” data types: booleans; numbers; strings (bytes or Unicode); arrays or mappings of simple data types; algebraic data types defined in your interpreter.
Examples of data types not considered “simple”: function expressions; a subset of strings representing valid programs in some language (unless the subset is validated by your interpreter); generalized algebraic data types. (This restriction is intended to disqualify trivial answers such as the identity function in Agda.)
Your input format must include some way to express arbitrarily sized natural numbers, in a reasonable representation of your choice (e.g. arrays or strings of unary, binary, or decimal digits, or directly as big integers if your host language has them). Your output format must include at least two values, distinguishable from each other, to represent “true” and “false”. Whether the formats can express anything else is up to you.
You may interpret programs in any target language, existing or new, under three conditions:
Your interpreter must be observably deterministic: for a given program and input, you must always produce the same output.
Your interpreter must not be Turing-complete. Explain why this is the case—for example, it might be Turing incomplete because the interpreter eventually halts on every program and input, or because its halting problem is otherwise decidable.
As a minimum standard of usefulness, your target language must be able to express every polynomial-time function from natural numbers to booleans. Explain why this is the case. (To be clear, “polynomial-time” is defined over the length of the input in binary bits, even if your chosen representation of naturals is less efficient.)
Whether any other functions are expressible is up to you—for example, you might design your language around the primitive recursive functions, the elementary functions, or Gödel’s System T, each of which includes all polynomial-time functions.
Your interpreter may be written in any existing host language. You may assume it runs on an ideal machine with unlimited memory and pointers big enough to access it.
This is code-golf: make the shortest interpreter you can!
Clarifications
I believe these points follow from the requirements, but they’re listed here in the hope of being helpful. Feel free to request additional clarifications in the comments.
As per our default rules, your interpreter will be a program or function that follows our usual conventions for input and output. However, programs in your target language are not bound by these rules—for example, if you decide that programs in your target language will be snippets that perform I/O by accessing a predefined variable, that is fine, as long as your interpreter translates this convention by (say) automatically reading from STDIN to the variable at startup and writing the variable to STDOUT at exit. (This can be viewed as a natural consequence of our policy that languages are defined by their interpreters.)
You may of course use any data types you want within your language, as long as the program, input, and output types are simple data types.
Your interpreter must be prepared to accept anything in your chosen simple data type for programs as a program. Your interpreter may of course perform extra validity checks on the program and throw errors or fall back to some default behavior or do something else that respects the requirements—but you may not impose extra validity constraints on programs purely at the specification level. Writing “
eval
but you’re not allowed to pass in anything except deterministic programs that halt” does not solve this challenge.Because of the determinism requirement, an interpreter that works by executing arbitrary code with a timeout in seconds is unlikely to be valid. (Doubly so if it leaves programs enough wiggle room to disable or evade the timeout in some underhanded way.)
Although I
am willing to bemay already have been proven wrong, my expectation is that solving this challenge will require doing some actual work to interpret a language. I’m not looking for solutions that put in 1% of this work to satisfy 80% of the requirements, whatever that means—that wouldn’t be fair to those who put in the effort to solve the full challenge as written.I updated the challenge with a requirement for the representation of natural number inputs to be “reasonable” after realizing there was a loophole using an unreasonable representation of naturals. Specifically: if we enumerate all polynomial-time functions as \$p_0, p_1, p_2, \dotsc\$, the unreasonable representation of \$n \in \mathbb N\$ as \$(p_0(n), p_1(n), \dotsc, p_{n-1}(n))\$ allows any polynomial-time function \$p_m\$ to be “programmed” as \$\{0 \mapsto p_m(0), 1 \mapsto p_m(1), \dotsc, m \mapsto p_m(m), n \mapsto n_m\text{ for }n > m\}\$, with every output hard-coded into either the program or the input! (I don’t think any of the existing answers have tried to exploit such an unreasonable representation.)
eval
? \$\endgroup\$