How it works? It works exactly as you described. And the behavior you described is perfectly expected. Let's see.
Nothing prevents the user from typing whatever this person wants, even not a number. If you need to write code guarded against incorrect input, you need to read not even a number, but the string, try to parse it into a number in the required domain of values and handle all the unsuitable input accordingly.
The purpose of subrange types is completely different. This is a static (that is, based on compile-time data) feature. In other words, the valid range is known statically. First, it allows the compiler to choose the underlying integer type automatically, based on the range known from the code during compile-time. Also, it makes necessary compile-time checks and validates that the range and operations like assignment or comparison operations are compatible. This check can work with constants or immediate constants (the examples of immediate constants are created when you write the literals 41 and 1 under your IF
and REPEAT… UNTIL
) statements. You have described one of the situations.
In other words,
VAR
j: 1..40;
//..
j := 41; // compile-time error, failure to build the code
//..
IF (j <= 40) AND (j >= 1) //... compile-time warning: it is statically
// analyzed that you cannot do, for example, assignment j := 41,
// therefore, the comparison operator will always return true,
// so, the IF condition is always met
In this sense, the subrange types are extremely useful.
The data entered by the user is the run-time data. It has nothing to do with the subranges. In the case of your subrange type, the input is interpreted according to the underlying type created during the build of the code.