summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
commit44505aa5b75b1046f13f2bc2a52fe0b16d975838 (patch)
treecc558485c807ac3842cd73943cf5e294c4a307de
parent2560096fb4636263798ae56c88687973c72cdf6e (diff)
downloadSimple-44505aa5b75b1046f13f2bc2a52fe0b16d975838.tar.gz
Simple-44505aa5b75b1046f13f2bc2a52fe0b16d975838.zip
Chapter 30: Text Formatting and Diagnosticslinear-chapter30
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--Makefile18
-rw-r--r--README.ja.md2
-rw-r--r--README.md2
-rw-r--r--chapter30.iml (renamed from chapter29.iml)4
-rw-r--r--docs/chapter30/30-grammar.ja.md181
-rw-r--r--docs/chapter30/30-grammar.md254
-rw-r--r--docs/chapter30/README.ja.md35
-rw-r--r--docs/chapter30/README.md240
-rw-r--r--docs/chapter30/construction-groups.md241
-rw-r--r--docs/chapter30/deferred-construction.md385
-rw-r--r--docs/chapter30/escape-flow.md147
-rw-r--r--docs/chapter30/examples/A_helloWorld.smp (renamed from src/test/smp/examples/A_helloWorld.smp)0
-rw-r--r--docs/chapter30/examples/BubbleSort.smp (renamed from src/test/smp/examples/BubbleSort.smp)8
-rw-r--r--docs/chapter30/examples/Capitalize.smp (renamed from src/test/smp/examples/Capitalize.smp)3
-rw-r--r--docs/chapter30/examples/Dijkstra.smp (renamed from src/test/smp/examples/Dijkstra.smp)8
-rw-r--r--docs/chapter30/examples/FileIO.smp (renamed from src/test/smp/examples/FileIO.smp)6
-rw-r--r--docs/chapter30/examples/README.md71
-rw-r--r--docs/chapter30/examples/TicTacToe.smp133
-rw-r--r--src/main/c/simple_crt.c124
-rw-r--r--src/main/java/com/seaofnodes/simple/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Coalesce.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/IFG.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/InstSelect.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Machine.java12
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ParseAll.java21
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java51
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/SourceError.java16
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CallNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/FldOffNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/FormatNode.java33
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoadNode.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ScopeNode.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Allocations.java20
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/ControlFlow.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Declarations.java21
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Expressions.java41
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Functions.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Lexer.java139
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Parser.java23
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/README.md2
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/References.java46
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Statements.java27
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Strings.java59
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Structs.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Types.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/package-info.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeConAry.java15
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeConAryB.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeInteger.java10
-rw-r--r--src/main/smp/sys.smp2
-rw-r--r--src/main/smp/sys/ary.smp26
-rw-r--r--src/main/smp/sys/aryi64.smp25
-rw-r--r--src/main/smp/sys/aryu8.smp45
-rw-r--r--src/main/smp/sys/io.smp75
-rw-r--r--src/main/smp/sys/libc.smp25
-rw-r--r--src/main/smp/sys/rt.smp3
-rw-r--r--src/main/smp/sys/rt/simple_args.smp22
-rw-r--r--src/main/smp/sys/text.smp98
-rw-r--r--src/test/java/com/seaofnodes/simple/BoundaryTest.java12
-rw-r--r--src/test/java/com/seaofnodes/simple/BrainFuckTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter04Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter05Test.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter07Test.java18
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter08Test.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter09Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter10Test.java9
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter12Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter13Test.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter15Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter16Test.java4
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java23
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter18Test.java46
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter19Test.java36
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter20Test.java14
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter21Test.java27
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter22Test.java11
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter23Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter24Test.java7
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java26
-rw-r--r--src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java14
-rw-r--r--src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/EscapeFlowTest.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/FinalConstructionTest.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/Int32Test.java94
-rw-r--r--src/test/java/com/seaofnodes/simple/MergeSortTest.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/MinusNodeTest.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java191
-rw-r--r--src/test/java/com/seaofnodes/simple/PartialReadTest.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java29
-rw-r--r--src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java32
-rw-r--r--src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java8
-rw-r--r--src/test/java/com/seaofnodes/simple/Simple.java30
-rw-r--r--src/test/java/com/seaofnodes/simple/StreamIOTest.java138
-rw-r--r--src/test/java/com/seaofnodes/simple/StringFormatTest.java158
-rw-r--r--src/test/java/com/seaofnodes/simple/TerminalInputTest.java184
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java53
-rw-r--r--src/test/java/com/seaofnodes/simple/TicTacToeTest.java92
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java70
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/extern_data.c7
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/int32.c26
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/runtime_args.c35
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/sort.c4
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/stream_io.c49
-rw-r--r--src/test/java/com/seaofnodes/simple/progs/stringHash.c2
-rw-r--r--src/test/smp/separate/client/Main.smp2
115 files changed, 3915 insertions, 482 deletions
diff --git a/Makefile b/Makefile
index ec59a033..e4537ca4 100644
--- a/Makefile
+++ b/Makefile
@@ -155,7 +155,19 @@ RELEASE:= build/release
TEST_SYS:= $(OBJS)/lib_$(CPU_ABI)/sys.o
RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o
RELEASE_CRT:= $(RELEASE)/$(CPU_ABI)/simple_crt.o
+RELEASE_ARGS:= $(RELEASE)/$(CPU_ABI)/simple_args.o
C_CALL_CONV:= $(if $(filter win64,$(CALL_CONV)),ms_abi,sysv_abi)
+GAME_DIR:= build/examples/$(CPU_ABI)
+TICTACTOE:= $(GAME_DIR)/TicTacToe$(if $(filter Windows_NT,$(OS)),.exe,)
+
+# Build a runnable game without starting it. Native outputs are ABI-specific;
+# WSL can use CLZDIR=build/classes-linux to keep its Java build separate too.
+.PHONY: tictactoe
+tictactoe: $(TICTACTOE)
+
+$(TICTACTOE): docs/examples/TicTacToe.smp $(CLZDIR)/test/.ttag $(RELEASE_SYS) $(RELEASE_CRT) Makefile
+ @echo "building " $@
+ @$(JVM) com.seaofnodes.simple.Simple --link --cpu $(CPU_PORT) --abi $(CALL_CONV) -L $(dir $(RELEASE_SYS)) -o $(GAME_DIR)/TicTacToe.o $<
$(TEST_SYS): $(CLZDIR)/main/.mtag $(simple_driver) $(SYS_SMPS) | $(CLZDIR)/test/.ttag
@echo "simping " $@
@@ -167,7 +179,11 @@ $(RELEASE_SYS): $(OBJS)/lib_$(CPU_ABI)/sys.o
@cp $< $@
@cp -r $(dir $<)sys $(dir $@)
-$(RELEASE_CRT): src/main/c/simple_crt.c
+$(RELEASE_ARGS): src/main/smp/sys/rt/simple_args.smp $(CLZDIR)/main/.mtag $(simple_driver) Makefile | $(CLZDIR)/test/.ttag
+ @echo "simping " $@
+ @$(JVM) com.seaofnodes.simple.Simple --cpu $(CPU_PORT) --abi $(CALL_CONV) --norun -o $@ $<
+
+$(RELEASE_CRT): src/main/c/simple_crt.c $(RELEASE_ARGS) Makefile
@echo "compiling " $@
@[ -d $(dir $@) ] || mkdir -p $(dir $@)
@gcc -c -O2 -DCALL_CONV=$(C_CALL_CONV) -o $@ $<
diff --git a/README.ja.md b/README.ja.md
index 41c6b4ba..ae2dde4d 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -69,7 +69,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っã�
* [第28a章](docs/chapter28a/README.ja.md):モジュールと遅延クラス初期化
* [第28b章](docs/chapter28b/README.ja.md):分離コンパイル
* [第29章](docs/chapter29/README.ja.md):技術的負債の整理
-* 第30章:作業中
+* [第30章](docs/chapter30/README.ja.md):作業中
付録:[ネイティブライブラリ](docs/appendices/native-libraries.md)、
[最適化とバックエンドの調整](docs/appendices/optimizer-tuning.ja.md)。
diff --git a/README.md b/README.md
index da9601e3..bb8592e7 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ The Simple language is styled after a subset of C or Java.
* [Chapter 28a](docs/chapter28a/README.md): Modules and Lazy Class Initialization
* [Chapter 28b](docs/chapter28b/README.md): Separate Compilation
* [Chapter 29](docs/chapter29/README.md): Tech Debt Cleanup, Partial Objects
-* Chapter 30: Text Formatting and Diagnostics
+* [Chapter 30](docs/chapter30/README.md): Text Formatting and Diagnostics
Appendices: [Native libraries](docs/appendices/native-libraries.md),
[optimizer and backend tuning](docs/appendices/optimizer-tuning.md).
diff --git a/chapter29.iml b/chapter30.iml
index 7905a62c..793144ee 100644
--- a/chapter29.iml
+++ b/chapter30.iml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
- <output url="file://$MODULE_DIR$/../build/idea/production/chapter29" />
- <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter29" />
+ <output url="file://$MODULE_DIR$/../build/idea/production/chapter30" />
+ <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter30" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
diff --git a/docs/chapter30/30-grammar.ja.md b/docs/chapter30/30-grammar.ja.md
new file mode 100644
index 00000000..13fd500d
--- /dev/null
+++ b/docs/chapter30/30-grammar.ja.md
@@ -0,0 +1,181 @@
+# 第30章の文法
+
+[English](30-grammar.md) | 日本語
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? IDENTIFIER '=' functionExpression ';' ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' IDENTIFIER | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' IDENTIFIER callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | STRING_LITERAL
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) ;
+STRING_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
diff --git a/docs/chapter30/30-grammar.md b/docs/chapter30/30-grammar.md
new file mode 100644
index 00000000..dca3b451
--- /dev/null
+++ b/docs/chapter30/30-grammar.md
@@ -0,0 +1,254 @@
+# Grammar for Chapter 30
+
+English | [日本語](30-grammar.ja.md)
+
+The handwritten recursive-descent implementation lives in
+[`com.seaofnodes.simple.parser`](../../src/main/java/com/seaofnodes/simple/parser/).
+The production-to-code map below the grammar describes where syntax ends and
+name resolution or graph construction begins. This grammar documents the
+language; it is not input to a parser generator.
+
+`int` is an alias for `i32`; `i64` explicitly selects 64-bit integer storage.
+This alias does not change literal inference or intermediate arithmetic.
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? memberName '=' functionExpression ';' ;
+memberName : IDENTIFIER | primitiveType ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+// At class/struct member scope, memberName also permits primitive type names.
+// Ordinary locals and parameters still require a non-keyword IDENTIFIER.
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' memberName | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' memberName callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | stringExpression
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+// The f suffix also accepts an integer-shaped token, e.g. 12f.
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
+ | DIGIT+ 'f' ;
+// String text uses a lexical mode: whitespace/comments are literal here.
+// Interpolation re-enters the ordinary expression grammar, recursively.
+stringExpression : '"' (STRING_TEXT | ESCAPE | '%%' | interpolation)* '"' ;
+interpolation : '%' format? '(' assignment ')' ;
+format : FORMAT_FLAG* FORMAT_WIDTH? ('.' FORMAT_WIDTH?)? FORMAT_KIND ;
+// Flags: - + space # 0. Width/precision: decimal digits, no expressions.
+// Kinds: d i u o x X f F e E g G a A c s b.
+// Duplicate/inapplicable flags and precision on c/b are rejected.
+// STRING_TEXT excludes double quote, backslash, and percent.
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"%] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
+
+## Reading the parser alongside the grammar
+
+All paths below are within `src/main/java/com/seaofnodes/simple/parser/`.
+The components share one `Parser` instance containing the cursor, active scope,
+compilation unit, and graph utilities. They are package-private collaborators;
+there is no inheritance chain, AST, or additional compiler pass.
+
+| Productions | Implementation |
+| --- | --- |
+| `program` | [`Parser.parse`](../../src/main/java/com/seaofnodes/simple/parser/Parser.java): establish the source-unit scope, parse its initializer, resolve remaining file names, publish its class |
+| `statement`, `block`, loops, jumps, `returnStatement` | [`Statements`](../../src/main/java/com/seaofnodes/simple/parser/Statements.java), starting at `parseStatement` |
+| `declaration`, `binding`, `bindingModifier` | [`Declarations`](../../src/main/java/com/seaofnodes/simple/parser/Declarations.java): declaration/expression lookahead, then each binding and initializer |
+| `structDeclaration`, `constructorDeclaration` | [`Structs`](../../src/main/java/com/seaofnodes/simple/parser/Structs.java), starting at `parseStruct` and `parseConstructorDeclaration`; field discovery and initialization checks follow these entry points |
+| `type`, `typeName`, `functionType` | [`Types`](../../src/main/java/com/seaofnodes/simple/parser/Types.java): `parseType` and `parseFunctionType`; qualified-name lookup lives in `typeName`, `dottedName`, and `rootedName` |
+| `assignment` through `primaryExpression` | [`Expressions`](../../src/main/java/com/seaofnodes/simple/parser/Expressions.java): `parseAssignment`, `parseExpression`, `parseLogical`, `parseBitwise`, `parseEquality`, `parseComparison`, `parseShift`, `parseAddition`, `parseMultiplication`, `parseUnary`, `parsePrimary` |
+| `postfixExpression`, member selection and updates | `Expressions.parsePostfix` / `parsePostfixMethod` dispatch to [`References.parseField`](../../src/main/java/com/seaofnodes/simple/parser/References.java); `References.parseName` handles bare names, implicit receivers, and forward references |
+| `functionExpression`, `parameterList`, calls, `argumentList` | [`Functions`](../../src/main/java/com/seaofnodes/simple/parser/Functions.java): `parseFunction`, `parseConstructor`, shared `parameters` and `arguments`, then `parseBody` |
+| `newExpression` | [`Allocations.parseNew`](../../src/main/java/com/seaofnodes/simple/parser/Allocations.java); the allocation-helper graph builder follows the expression routines |
+| `stringExpression`, `interpolation` | [`Strings.parse`](../../src/main/java/com/seaofnodes/simple/parser/Strings.java), using `Lexer.stringPart` / `stringFormat`; normal expression parsing resumes inside each interpolation |
+| Literals, identifiers, comments, token matching | [`Lexer`](../../src/main/java/com/seaofnodes/simple/parser/Lexer.java); it also supplies source-location snapshots used by diagnostics and serialized IR |
+
+`ControlFlow` shares guarded scope merging between `if`, `?:`, `&&`, and `||`.
+`Conversions` shares binding, field, and result conversions. These
+operations are compiler semantics rather than additional grammar productions.
+`Parser` retains the type registry and the small control/memory/peephole helpers
+used by several productions and by the rest of the compiler.
+
+There are a few deliberate differences between a syntax-only parser and this
+one-pass IR builder:
+
+- A statement beginning with `{` might be a block or a function-type binding.
+ `Declarations` and `Types` use speculative parsing and cursor restore;
+ qualified type lookup also stops before a suffix that belongs to an expression.
+- Bare assignment, field assignment, and compound updates share the grammar's
+ precedence but use different IR paths. `References` decides between an SSA
+ local, a constructor field's local value, and an actual memory access. A
+ function-valued field may also supply an implicit receiver to a call.
+- `for` updates are scanned first and parsed after the body, where their effects
+ belong. Explicit constructor bodies are scanned first and parsed after field
+ discovery, so later fields have local slots. Both use `Lexer.skipTo`, which
+ ignores delimiters inside comments and quoted text.
+- `Structs.parseInitializer` parses a file or struct body as a function while
+ discovering fields. `Functions.parseBody` still owns function scope/return
+ construction; moving it does not change constructor or memory semantics.
+
+The syntax chart does not describe all name/type restrictions. In particular,
+prefix updates require an already-known non-pointer binding, and a private-class
+privacy policy is separate from recognizing a qualified type name. The existing
+same-named function binding inside a struct is also recognized as a constructor;
+`new Name = { ... };` is the explicit constructor spelling above.
diff --git a/docs/chapter30/README.ja.md b/docs/chapter30/README.ja.md
new file mode 100644
index 00000000..1d275a5c
--- /dev/null
+++ b/docs/chapter30/README.ja.md
@@ -0,0 +1,35 @@
+# 第30章:テキスト整形と診断
+
+[English](README.md) | 日本語
+
+[前の章:第29章 — 技術的負債の整理](../chapter29/README.ja.md)
+
+ASCII の[三目並べ](examples/TicTacToe.smp)を題材に、コンパイラと
+標準ライブラリを改善します。ゲーム自体ではなく、実装中に必要になった
+言語機能、診断、ランタイムが章のテーマです。実行可能な例は
+[`examples`](examples/)にあります。
+
+- `sys.io.getch()` による Enter 不要の1バイト入力と、端末モードの復元。
+- EOFまでの可変長読み込みと、部分書き込み・割り込みの再試行。
+ 読み込みエラーは `null`、書き込みエラーは `-1` と errno で通知します。
+ `aryu8` と `aryi64` の長さは `u32` とし、拡張時の桁あふれを防ぎます。
+- `file:line:column`、ソース行、位置マーカーを含む構文・型エラーの診断。
+- `int` を `i32` の別名に変更。型推論と中間演算の規則は維持し、
+ 代入時に32ビットへ縮小します。ポインタの幅は変わりません。
+- `"CPU plays %(reply+1).\n"` のような文字列補間と、`sys.text` の型別変換。
+ 整数などの整形は Simple で実装し、浮動小数点変換だけ C を呼び出します。
+- `sys.rt.simple_args` がネイティブの起動引数を Simple の配列へコピー。
+- 関数ごとのレジスタ割り当てと再試行。レジスタを使わない依存を生存解析から
+ 除外し、レジスタ破壊をまたぐ必要な転送を分割時に保持します。
+
+リポジトリのルートからゲームをビルドします。
+
+```sh
+make -C chapter30 tictactoe CTAGS=
+```
+
+Windows では `chapter30/build/examples/x86_64_v2_win64/TicTacToe.exe`、
+Linux では `chapter30/build/examples/x86_64_v2_SystemV/TicTacToe` を実行します。
+詳しい書式指定、テスト、WSL でのビルドは[英語版](README.md)を、
+バックエンドなどの未完了の作業は[ロードマップ](../roadmap.ja.md)
+を参照してください。
diff --git a/docs/chapter30/README.md b/docs/chapter30/README.md
new file mode 100644
index 00000000..7cc0c45e
--- /dev/null
+++ b/docs/chapter30/README.md
@@ -0,0 +1,240 @@
+# Chapter 30: Text Formatting and Diagnostics
+
+English | [日本語](README.ja.md)
+
+[Previous: Chapter 29 — Tech Debt Cleanup](../chapter29/README.md)
+
+This chapter uses a small game to drive improvements in the compiler and the
+`sys` standard library. The game provides a concrete workload; the chapter's
+subject is the language and library work uncovered.
+
+The first program is ASCII [tic-tac-toe](examples/TicTacToe.smp).
+The player moves first with single keystrokes; the CPU responds, occupied
+squares are rejected, and play ends with a win or draw.
+Runnable programs, including sorting, file I/O, and shortest-path examples,
+live in [`examples`](examples/).
+
+## Terminal input and diagnostics
+
+[`sys.io.getch()`](../../src/main/smp/sys/io.smp) reads one byte without waiting for
+Enter or echoing it. A C adapter changes and restores the terminal mode;
+redirected files and pipes also work. It returns `0..255`, or `-1` with
+`sys.libc.errno()` set to `sys.libc.EOF` or an OS error. Escape and UTF-8
+sequences arrive as multiple bytes.
+
+The game echoes accepted moves, ignores Enter, and quits on `q`, Escape,
+Ctrl-C, Ctrl-D, or end of input.
+
+We also exposed a parser bug: branch guards from one function could affect the
+next function. Function boundaries now isolate those guards. Loop-path merges
+also preserve forward references discovered after a break or continue.
+
+The CLI reports parser and type errors as `file:line:column: error: message`,
+followed by the source line and an underscore-and-caret pointer - suitable for
+older text editors (VI, Emacs). Language errors now exit with status 1 instead
+of throwing an exception; unexpected compiler errors keep their Java stack
+traces. Quite a few error reporting points are cleaned up: malformed numbers,
+invalid escapes, unterminated comments or quoted text are a few.
+
+## Stream I/O and resizable arrays
+
+`sys.io.read(fd)` grows its buffer and continues through short reads until
+EOF. It returns an exact-sized byte array, empty for immediate EOF, or `null`
+on error with `sys.libc.errno()` preserved. Partial input is discarded on an
+error. `stdin()` uses the same behavior.
+
+`write`, `writeOff`, and `aryu8.write` write the complete requested data,
+retrying partial writes. Reads and writes retry `EINTR`; other errors return
+to the caller. Writes return a 64-bit byte count, or `-1` with errno set; a
+failure can follow a successfully written prefix. A zero-progress write
+reports `EIO`. `writeOff` rejects offsets past the array end with `EINVAL`,
+caps the length at the remaining bytes, and accepts empty slices at the end.
+
+The buffering and retry loops are Simple code. C supplies access to errno and
+its platform constants; the native read/write bindings use 64-bit counts and
+results on the supported targets. Tests cover files, pipes, empty and large
+inputs, and injected short transfers, interruptions, and failures.
+
+`aryu8` and `aryi64` store their lengths as `u32`, matching built-in array
+lengths. Growth handles empty backing arrays, uses existing capacity when it
+fits exactly, and calculates the next capacity in 64-bit arithmetic before
+narrowing. Requests beyond the `u32` length limit are rejected: stream reads
+return `null` with `EOVERFLOW`, while direct resizable-array growth exits with
+status 1. The tests check arithmetic at the unsigned boundary without allocating
+multi-gigabyte buffers.
+
+## Default integer type
+
+`int` now aliases `i32`. Its fields and array elements occupy four bytes;
+assignments and updates use the existing signed 32-bit narrowing rules.
+Explicit `i64` remains available for wide arithmetic and native addresses.
+Literal inference, `var`/`val` inference, and intermediate arithmetic retain
+their existing rules: `int x=2147483647; return x+1;` produces `2147483648`,
+whereas assigning that sum back to `x` produces `-2147483648`.
+
+The standard library spells out `i64` where it needs full-width formatting or
+native addresses/sizes. Resizable-array lengths, growth counts, and indices
+use explicit `u32`.
+
+Function entries normalize narrow signed/unsigned parameters, including
+stack arguments, using the backend extension operations. This also applies
+to internal Simple calls. Loop type widening preserves signed and unsigned
+32-bit bounds of values narrowed on every assignment.
+
+## Strings and formatting
+
+Strings can embed expressions from the surrounding scope:
+
+```java
+sys.io.p("CPU plays %(reply+1).\n");
+sys.io.p("score=%04d(score), average=%.2f(average)\n");
+sys.io.p("name=%-12.8s(name), key=%c(key)\n");
+```
+
+`%(expression)` selects decimal integer, floating-point, or byte-string
+formatting from the expression's type. Booleans are integer aliases, so the
+default prints `0` or `1`; `%b(expression)` prints `false` or `true`.
+Expressions execute once, from left to right. Each value is formatted before
+evaluating the next expression. The result is an ordinary immutable `u8[~]`
+with its exact byte length. Strings without embedded expressions remain
+constants.
+
+An explicit format has the form `%[flags][width][.precision]kind(expression)`:
+
+| Kind | Argument and output |
+| --- | --- |
+| `d`, `i` | Integer, signed decimal |
+| `u`, `o`, `x`, `X` | Integer, unsigned decimal, octal, or hexadecimal; negative values use their 64-bit representation |
+| `f`, `F`, `e`, `E`, `g`, `G`, `a`, `A` | Float, using the corresponding C format |
+| `c` | Integer, one byte from its low eight bits |
+| `s` | Byte string, including embedded zero bytes |
+| `b` | Integer, `false` for zero and `true` otherwise |
+
+Flags are `-` (left alignment), `+` (explicit sign), space (positive sign
+padding), `#` (alternate form), and `0` (zero padding), where applicable.
+Width and precision are literal decimal integers. String precision limits
+bytes; integer precision supplies a minimum digit count. `c` and `b` allow
+width and left alignment, but no precision. Default floating-point formatting
+uses `g` with 17 significant digits; explicit floating formats use C defaults
+unless a precision is given. Positional arguments, `*` widths, C length
+modifiers, and `%n` are not supported.
+
+Write `%%` or `\%` for a literal percent. Every other `%` must introduce a
+complete format with parentheses. Inside those parentheses the normal
+expression parser resumes, so nested strings work directly:
+
+```java
+val text = "outer %("inner %(value)")";
+```
+
+[`sys.text`](../../src/main/smp/sys/text.smp) provides explicitly named converters:
+`int`, `flt`, `f32`, `f64`, `char`, `bool`, and `hex`. For example,
+`sys.text.int(17)` returns `"17"`. Primitive type names are allowed as member
+names and in member declarations; they remain reserved for local variables and
+parameters. `sys.io.p` continues to require a byte string.
+
+`Strings` builds ordinary calls to `sys.text`, with normal module lookup,
+class initialization, and control/memory edges. A small `FormatNode`
+selects the default typed formatter as normal type flow resolves an expression,
+including forward references; it folds away in valid programs.
+
+The formatting library is written in Simple: `begin` creates an `aryu8`,
+`integer` and `string` append formatted values, and `end` copies the populated
+bytes into the immutable result. The integer formatter covers signed and
+unsigned 64-bit values, including minimum `i64`, and handles signs, prefixes,
+width, and precision. Character and boolean formatting use the same builder.
+`aryu8.num` shares the integer formatter, including its full-range handling.
+Only binary floating-point conversion calls C, through one fixed-signature
+adapter returning a byte string. There is no boxing or variadic Simple call.
+
+`sys.text` uses no interpolated strings, including in its named converters, so
+the library builds from source without a staged bootstrap. Non-float
+formatting can also run in the IR evaluator. The lexer shares its string and
+format scanner with deferred constructor-body and loop-update scanning.
+
+## Runtime support in Simple
+
+Native startup also uses Simple: [`simple_args.smp`](../../src/main/smp/sys/rt/simple_args.smp)
+allocates the argument array and copies each argument into a byte array. The
+C entry adapter calls this compiled routine before `simple_main`; C supplies
+only access to `argv[i]`, `strlen`, and `memcpy`. The converter fills every
+nullable array slot before publishing the result as `u8[~][]`, including the
+program name at index zero. It builds independently of `sys`, so even programs
+without standard-library imports use it.
+
+The `sys.rt` module holds Run Time support, like Java's `rt.jar` or C's `crt.o`.
+Make emits `simple_args.o` alongside `simple_crt.o`; the native linker driver
+includes both runtime objects automatically.
+
+## Register allocation
+
+The allocator handles each function independently, retaining its completed
+register assignments while reusing scratch space for the next function.
+Building live ranges, constructing interferences, coalescing, coloring, and
+splitting all run on that function's scheduled blocks. The eight-round limit
+applies per function. A hard register-mask conflict in one function therefore
+cannot prevent another function from reaching coloring or consume its retry
+budget.
+
+Two changes avoid unnecessary retries. First, liveness ignores inputs whose
+operand register mask is null: these are metadata or scheduling dependencies,
+not runtime register uses. In particular, a static call's unused hidden class
+argument must not keep its class-pointer constant live across other calls and
+trigger rematerialization.
+
+Second, splitting retains both transfers when a value must move out of a fixed
+register before a kill and back into it afterward. A buggy attempt at early
+coalescing would undo a split needed to dodge the kill. Interference-aware
+coalescing decides when they can be joined.
+
+Loop self-conflicts initially try cold transfers. If that does not separate
+the ranges, the fallback also splits backedge inputs. Distinct backedge nodes
+can still share a range through two-address instructions; copying only the Phi
+outputs leaves that conflict intact.
+
+The spill-ranking heuristic and calling conventions are unchanged. Saving a
+callee-save register can still require several coloring attempts as pressure
+changes. The [allocator regressions](../../src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java)
+check function independence in both scheduling orders, preservation across
+register kills, and dependencies that do not consume registers.
+
+## Related follow-up work
+
+[Narrow integer instruction selection](../roadmap.md#integer-instruction-widths)
+and [per-function calling conventions](../roadmap.md#per-function-calling-conventions)
+remain separate backend projects. C pointer types and compact references are
+also deferred; the `int` change does not shrink pointers.
+
+The [grammar and code map](30-grammar.md),
+[source layout](../../src/main/java/com/seaofnodes/simple/README.md), and
+[construction notes](construction-groups.md) describe the starting point.
+Open projects are listed in the [roadmap](../roadmap.md).
+
+## Build and test
+
+From the repository root:
+
+```sh
+make lib tests release CHAPTERS=chapter30 CTAGS=
+```
+
+Build the game without starting it:
+
+```sh
+make -C chapter30 tictactoe CTAGS=
+```
+
+The Windows/Cygwin build produces
+`chapter30/build/examples/x86_64_v2_win64/TicTacToe.exe`, which can be launched
+from Cygwin or PowerShell. Linux produces
+`chapter30/build/examples/x86_64_v2_SystemV/TicTacToe`.
+When building in WSL alongside Windows, keep Java build outputs separate:
+
+```sh
+make -C chapter30 tictactoe CLZDIR=build/classes-linux CTAGS=
+```
+
+IntelliJ has a `chapter30` module and a **chapter30 tests** run configuration.
+Build the native runtime and `sys` library with Make before debugging tests
+that need them; see [the IntelliJ setup](../intellij.md).
+Inherited test class names retain their historical identities.
diff --git a/docs/chapter30/construction-groups.md b/docs/chapter30/construction-groups.md
new file mode 100644
index 00000000..197a6082
--- /dev/null
+++ b/docs/chapter30/construction-groups.md
@@ -0,0 +1,241 @@
+# Construction: field types and incomplete references
+
+Instance constructors use ordinary field Loads and Stores. There are two checks:
+whether a field has a value, and whether an object reference is usable outside
+construction. Neither requires a collection of constructor-group identities.
+
+## Field initialization
+
+New produces private `TypeMem` containing the object's `TypeStruct`, with
+`TypeScalar.BOT` for fresh fields. Defaults emit ordinary Stores: zero for an
+ordinary integer, null for a nullable pointer, no Store for a required field.
+An assignment strongly updates its field in private memory. Phi meets the
+incoming memories; a missing live arm survives because `BOT.meet(7) == BOT`.
+An initialized unknown integer has `TypeInteger.BOT`, not `TypeScalar.BOT`.
+Arrays retain their language-level zero filling.
+
+One `#selfMem` binding carries this memory through scopes and returns. A Load
+checks its incoming field type. Constructor completion checks the merged Return
+memory; the explicit constructor receives the declaration initializer's merged
+result, including early exits. There is no backwards initialization query,
+per-field parser state, or store-all instance exit.
+
+The existing Iter/Opto passes propagate these types; TypeCheck reports errors.
+There is no extra construction type solve or retained source-use list. An
+initialized field carried unchanged around a loop needs the optimistic SCCP
+solution. Numeric fields in loop memory widen as scalar loop values do.
+Arithmetic, conversions, and numeric Stores preserve Scalar.BOT, so a live bad
+read cannot disappear merely because it is multiplied by zero. Genuinely unused
+reads may be removed by DCE.
+
+The allocator's ConstructNode retains the constructor's final private memory
+for its completion check. TypeCheck gathers these nodes during its existing
+error walk and removes checked boundaries before lowering. The pass removal
+exposes known failures; see [the late-check notes](deferred-construction.md).
+
+Internal construction memory parameters use actual allocation/initializer call
+edges; the hypothetical external caller does not supply their memory. They
+retain their memory family when there are no live callers. Function-valued
+fields retain their declared signatures in private summaries, avoiding repeated
+unfolding of a constructor whose returned memory contains its own function field.
+Class singleton initialization keeps its older store-at-exit implementation.
+
+## Partial references and readable fields
+
+`self` and incoming `C ^arg` parameters are incomplete references. `C? ^arg`
+also accepts null. Ordinary functions and constructors accept the qualifier on
+struct parameters; arrays cannot be qualified this way.
+
+- An allocation result is incomplete if any actual argument is incomplete.
+- With only completed arguments, including null, the result is complete.
+- An incomplete reference may be stored into self or passed to a `^` parameter.
+- Ordinary calls and stores into other objects require completed references.
+- A borrowed partial can read fields its incoming type proves initialized.
+ It cannot write fields, even non-finals. Exact local aliases of self retain
+ the original constructor's access to its current private memory.
+- An object-reference field read through a partial is conservatively
+ incomplete, with no initialized-field guarantees for its referent. There is
+ no inference of initialized-field requirements on `^` arguments.
+ Arrays are already initialized and cannot receive
+ incomplete elements, so an initialized array field can be used normally.
+
+For example:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { int n ->
+ child = new Child(self); // Incomplete result: self is incomplete.
+ // Retention does not authorize reading parent.payload through child.
+ payload = n;
+ };
+};
+val parent = new Parent(arg);
+val local = new Child(parent); // Complete argument, complete result.
+return local.parent==parent; // Lookup is allowed.
+```
+
+Here `Test` names the source unit. After the root Parent constructor completes,
+the entire cycle is usable: all members have initialized their own fields, and
+none could expose an incomplete reference to ordinary code during construction.
+The Child constructor still treats its `^par` as incomplete when the actual
+argument is complete. This public constructor also has unknown callers, whose
+partial arguments promise no initialized fields, so `par.payload` is unreadable.
+
+## Forward field views
+
+At a use of self, `InitViewNode` combines the pointer with that program point's
+private memory. Its TypeMemPtr contains declared types for initialized fields
+and Scalar.BOT for missing fields. The node retains the declaration separately
+from the incoming flow shape, so an exact self alias captured before a Store
+can receive a fresh view afterward. Earlier calls retain their earlier views.
+
+For example, a private helper can read `x` while `y` is still unset:
+
+```simple
+struct _C {
+ int~ x;
+ int~ y;
+ new _C = { int n -> x=n; y=_read(self); };
+};
+val _read = { _C ^p -> return p.x; };
+return new _C(arg).y;
+```
+
+Parm meets the incoming shapes using the normal type flow. If any live caller
+lacks `x`, Scalar.BOT survives and the Load reports the missing initialization.
+Unknown callers supply an empty field promise. Parsing must also start borrowed
+parameters at that bound: otherwise early folding of `p.x*0` could hide a bad
+read before SCCP. The hidden constructor receiver retains its declaration;
+its private memory supplies its read checks and precise field updates.
+
+Values still come from normal memory. Before a call borrowing self, EscapeNode
+borrow slices carry the current constructor Stores onto the ordinary call
+memory edge. These slices are aliasable and do not publish the private identity
+or its fields into the global escape sets. No hidden partial-memory arguments,
+field bitsets, additional passes, or recursive flow queries are introduced.
+
+InitView folds before TypeCheck when its input already has the same type and
+retains a structural partial view. Nested partial-only Construct wrappers also
+collapse. A Parm's current partial type alone is insufficient: inlining may
+later substitute a complete actual argument, which must not grant the borrowed
+parameter permission to write or publish that object. The completion form of
+Construct already folds once its memory proves initialization and its result
+type matches its input.
+
+TypeCheck checks the consumers and erases remaining InitViews along with the
+checked Construct boundaries in its existing walk. A surviving field view is
+not necessarily erroneous: a reader may need only the initialized subset.
+Construct remains necessary for
+final-field completion and for the partial qualifier of each source `new`.
+Alias Phis and nested reference loads are conservative; they do not acquire
+the original constructor's write permission. This prototype adds no general
+ban on overlapping allocations of the same class: only the owning receiver
+uses private memory, while borrowed reads use aliasable memory.
+
+## Aliases and loops
+
+Incomplete is a property of a value, not just the current contents of a parser
+Var. Consider `while (n--) { use(p); p = self; }`: the backedge introduces an
+incomplete value after `use(p)` has been parsed. `TypeMemPtr._partial` carries
+that restriction through ordinary forward typing. It is an independent
+two-point lattice: meet ORs the restriction and dual complements it. Copies,
+conversions, Stores, and Phis preserve it; a merge is incomplete if any live
+input is incomplete. A `^` formal stays incomplete even when its known callers
+are complete. The receiver's private memory identifies the own-self exception;
+another incomplete pointer of the same class has no such permission.
+
+A small `ConstructNode` wraps each source `new`. Its runtime value is the
+allocator result; its type adds the OR of that call's actual argument qualifiers.
+This distinguishes `new Child(self)` from `new Child(null)` even when both call
+the same constructor. Looking only at the shared callee Return would lose this
+distinction. The node disappears after TypeCheck, before lowering.
+
+A completed receiver gives a completed view of its pointer fields. Construction
+may retain incomplete references but cannot publish them, so reaching the root
+`new` boundary with complete arguments makes the entire reachable cycle usable.
+There is no group identity or traversal to mark members complete. Conversely,
+object-reference fields read through incomplete self remain conservative, even
+if the particular stored reference was complete.
+
+The existing optimistic SCCP solve also handles qualifier cycles. For
+`p = null; while (n--) p = new Link(p);`, no input introduces incompleteness,
+so `p` stays complete and a subsequent field read is allowed. A backedge carrying
+self instead makes the Phi and its uses incomplete.
+
+`Construction` selects private self memory during parsing; it retains no
+source-use lists or flow facts and performs no recursive Load/Phi/Call query. The old
+root and allocation bitsets are gone. There are no field-requirement sets,
+hidden `^` memory arguments, or runtime group objects.
+
+`PartialReadTest` exercises field reads, missing fields, forwarding, merged
+callers, loops, exact aliases, constructor retention, and borrowed write and
+publication restrictions. `ConstructionGroupTest` exercises cycles, complete/null arguments, prohibited
+lookups/publication, aliases, branch merges, complete and incomplete loop
+backedges, and declared restrictions with only complete callers, including an
+uninlined constructor through machine encoding. `ConstructorCompletionTest`,
+`FinalConstructionTest`, and `BoundaryTest` exercise field-memory semantics.
+The former inferred-requirement litmus tests are preserved, explicitly deferred,
+in `ConstructionRequirementsTest`. Separate compilation of `^` functions is
+still deliberately unsupported. The pointer qualifier has an object-file
+representation, but importing and checking the full construction contract is
+outside this experiment. Rebuild chapter 29 object files after the type-format
+change.
+
+## Deferred field-aware contracts (2026-11-07)
+
+Decision: do not implement inferred contracts now. Cliff considers the required
+infrastructure too large for the feature alone; revisit when a general
+backwards/liveness flow is justified, potentially for memory management.
+The later forward-field-view prototype above does not implement this contract
+discovery proposal. Separate compilation of `^` interfaces remains rejected.
+
+The intended extension would let functions and constructors receive partially
+initialized objects, read initialized fields, retain partial references with
+their restrictions, and write non-final fields. Only the original constructor
+would retain permission to initialize/reassign its own final fields; passing a
+pointer must not pass that permission. Final-field reads during construction
+would not establish value stability against later writes by that constructor.
+
+The proposed contract representation uses normal types, not a separate field
+bitset. For a function reading only `P.pi`, its unknown-caller default would be
+schematically `Partial{pi:declared-pi-type, name:TypeScalar.BOT}`. Scalar.BOT
+means no initialization guarantee, not a requirement that the caller leave the
+field unset. Required fields receive their declared types, covering every legal
+external caller. MEET with actual arguments preserves those guarantees only
+when the callers satisfy them; extra initialized fields supplied by locals do
+not strengthen the body beyond the external contract. TypeStruct/Field already
+meet field contents, and nested/cyclic types can represent nested contracts.
+TOP is not the missing-initialization marker.
+
+Cliff proposed discovering conservative, context-insensitive requirements after
+Iter and before SCCP. Freeze the resulting default argument types on functions;
+SCCP consumes them without refining the contracts. Reads on branches SCCP later
+eliminates may still impose requirements, as may reads following assignments.
+That rejection of some valid programs is acceptable for an opt-in `^` feature.
+Combine compatible requirement shapes with JOIN; combine possible caller states
+with MEET. Keep the contract distinct from the merged Parm type, and check each
+actual argument against it for a call-site diagnostic such as
+"foo requires Partial.pi to be initialized". Serialize contracts with declaration
+metadata so checking imported calls does not require loading bodies.
+
+The complication is discovering and propagating those contracts. Local SSA
+aliases share values; Phi demands reach every input. A demand on a loaded field
+becomes a nested receiver requirement. Calls substitute actual arguments for
+formals, and recursive forwarding needs a convergent propagation rule. Cyclic
+types represent recursive requirements but do not by themselves prevent a
+discovery algorithm from unfolding progressively deeper paths. Indirect calls
+need contracts available through their callable interfaces; returns need usable
+guarantees without retroactively strengthening an exported contract for a client.
+Stored references and helper writes also require a coherent account of private
+construction memory and object identity; that discussion was deliberately left
+unfinished. None of this requires H-M or bidirectional type inference, but it
+does require a substantial backwards demand analysis to handle well.
+
+The ignored ConstructionRequirementsTest retains useful litmus cases, not an
+approved implementation plan. Do not restart this work without a new request.
diff --git a/docs/chapter30/deferred-construction.md b/docs/chapter30/deferred-construction.md
new file mode 100644
index 00000000..104ab9e9
--- /dev/null
+++ b/docs/chapter30/deferred-construction.md
@@ -0,0 +1,385 @@
+# Deferring construction checks: experiment
+
+The [escape-flow continuation](escape-flow.md) supersedes the frozen-public
+fallback and remaining recursive-test failures described later in these notes.
+All four recursive examples now pass their original printed-IR expectations.
+The older audit below records the evidence that led to this change.
+
+The extra construction pass is removed from the working implementation.
+`Construction.check()`, its retained source-use lists, `Iter.types()`, and the
+separate constructor-Return walk are gone. Errors now belong to the existing
+TypeCheck walk; ConstructNode retains final private memory for completion checks.
+No additional whole-graph pass was added. Constructor rejection tests remain active.
+
+## Constant folding
+
+`TypeScalar.BOT` is not an initialized integer. Arithmetic, comparisons,
+negation, numeric conversions, and typed Stores must preserve it rather than
+manufacturing an integer or floating-point result. In particular, `x*0` and
+`x==x` cannot become constants while `x` has this type. An initialized unknown
+integer still permits those folds.
+
+The scan found and changed these related cases:
+
+- Multiplication by zero now folds in `compute`, after checking numeric inputs.
+ Integer strength reduction also waits for integer operands; an unfinished
+ loop Phi could eventually become a float.
+- Same-input comparisons fold in `compute`. Floating comparisons keep NaN
+ semantics; `x==x` is not necessarily true. The integer range implementation
+ of `!=` also had the equality answers reversed.
+- Arithmetic pushed through Phis uses each copied operation's `compute`,
+ including floating-point constants, instead of an unchecked integer cast.
+- Known array length offsets and element shifts fold in
+ `ConFldOffNode.compute`, rather than its `idealize`.
+- `Not`, unary minus, conversions, and Stores preserve unknown initialization.
+ `RoundF32` already passed that type through.
+
+This does not ban creating constant *operands* in rewrites such as `x*8` to
+`x<<3`. Memory forwarding still has cases that return a stored constant or an
+array's zero-filled element; these were inspected but not rewritten into a new
+memory analysis. `Load.idealize` retains a `Scalar.BOT` load.
+
+The changes exposed a rejected-Phi-fold worklist cycle. Factoring
+`Phi(bool & 7, integer & bool)` widens the result from `bool` to `[0-7]` and
+must fail. Building and then killing the candidate woke the same dependencies
+forever. The trial now computes its types before registering use edges.
+
+## Preserving the constructor body through optimization
+
+This regression must reject a live incomplete lookup:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { ->
+ child = new Child(self);
+ payload = child.parent == null;
+ };
+};
+return new Parent().payload;
+```
+
+`child` is incomplete because its constructor received incomplete `self`.
+Reading `child.parent` must therefore fail. The read supplies the final result;
+this is not a discarded local or dead-code issue.
+
+The decisive failure was private-memory Parm typing. During Iter, inlining
+removed a real caller while the function still had its unknown-caller edge.
+The special case returned MemTop anyway. That erased the memory merge, Store,
+and the bad Load feeding it, before TypeCheck could report the error.
+
+Parms now use their normal merge. Hidden initializer/constructor helpers have
+only actual allocator callers; the source-visible constructor binding denotes
+the allocator. Public allocators still receive unknown callers, so unused public
+constructor bodies remain checked. This also avoids poisoning a helper's private
+memory with a fictitious external default.
+
+Inlining presents a second issue: a complete actual pointer must not grant
+permission to read a restricted formal. A partial-only ConstructNode gives
+self and ^ parameters their body view independently of Parm typing. It remains
+until TypeCheck. Load's alias search recognizes that this view preserves the
+address, while compute/err continue to see the qualified pointer.
+
+A smaller regression reads an unwritten field through a self cycle:
+
+```simple
+struct C {
+ C next = self;
+ int~ x;
+ new C = { -> x = next.x; };
+};
+return new C().x;
+```
+
+Both examples now reject at TypeCheck. A positive regression initializes x to
+7, increments it in the constructor, and checks that the result folds to 8.
+
+The broader run exposed a function-lifetime bug during SCCP: unlinking calls
+could delete a body subsequently rediscovered from a type-level function
+reference. Returns are kept during the existing reset walk and released once
+call edges have been rebuilt. No additional graph walk is needed.
+
+Class-init dependency discovery also skips a CLInit whose receiver has not
+resolved to a class. A Scalar.BOT receiver is left for its Load's TypeCheck
+error, rather than dereferencing a null class name.
+
+The trial successfully uses the existing passes to prove a chain built from
+`null` complete. It also rejects direct `par.payload`, `par.payload*0`, and
+`int x = par.payload; value = x*0` after the arithmetic/conversion repairs.
+
+Genuinely unused reads are allowed to disappear under late-check semantics.
+Rejection tests must keep the read live. `testUnusedIncompleteReadCanDie` checks
+that an unused `par.payload` read is eliminated and the program returns 5.
+
+## Optimization differences and register allocation
+
+`Chapter18Test.testFcn9` exposed an independent escape bug. StopCU treated the
+class initializer's synthetic self as an object already held by an external
+caller. Its broad function-valued field type exposed the local x function even
+though the initializer never returned and x had no callers after inlining.
+Skipping that synthetic argument restores the empty escape set for x and the
+expected `return Top;`. Actual public-call arguments still escape normally.
+
+That correction exposed `Chapter18Test.testFcn8`: a Store computed from TOP
+memory interpreted its optimistic singleton bit as established private memory.
+It widened a function value to the declared signature, then narrowed its FIDX
+set when memory resolved. Store now returns TOP while its memory is TOP, so
+SCCP remains monotone.
+
+The String differences have a different cause. In Chapter19, the private
+_hashCodeString body dies; the surviving functions are the initializer,
+allocator, equals, and public hashCode. Its golden string now omits the two dead
+private-helper MemTop Returns and matches the current addition order.
+`Add.spine_cmp` breaks this tie by node ID. Strength reduction creates the Sub
+in IterPostOpto, after the byte Load (Sub#594 versus Load#504 in one traced copy),
+so the Sub sorts to the left. This is node ordering, not a lost value type.
+
+In the Chapter20/21 fixtures, _hashCodeString really escapes through the implicit
+return, and hashCode contains an inlined copy. Before the allocator repair, both loops needed an extra x86
+register copy with the new addition operand order. A disposable probe
+swapped only `((hash<<5)-hash) + byte` to `byte + ((hash<<5)-hash)` after Unlink:
+no types or live functions changed, but the weighted split score fell from 37
+to 21. Each hash body fell from 17 to 9; the allocator stayed at 3.
+
+Reconstructing the earlier source by reversing the saved construction-pass
+removal patch reproduces the expected 29. Its standalone _hashCodeString costs
+17, inlined hashCode costs 9, and allocator costs 3. Before the allocator repair,
+these costs were 17, 17, and 3. Both versions contain two hash loops; no inlining decision changed.
+Current code emits two fewer functions, the dead initializer/constructor helpers,
+which contributed zero. Thus the entire increase is one loop-weighted register
+copy in the inlined hashCode body, not another retained function or stack spill.
+
+With the old operand order, the byte Load can write the loop's hash register
+and Add accumulates the shifted/subtracted value into it. With the new order,
+Add's destructive input is the Sub result. Its two-address tie plus the Phi
+backedge joins the shift/Sub/Add chain to the old hash live range, which overlaps
+the shifted value at the subtraction. `RegAlloc.splitSelfConflict` inserts a
+`use/self/two` copy before Add. The existing commute heuristic in `splitByLoop`
+is not reached: self conflicts take an earlier splitting path. Commuting Add
+before splitting is the concrete optimization opportunity.
+
+The allocator now does this in `splitSelfConflict`, using the existing failed-
+range traversal and scratch list. It commutes two-address operators when the
+RHS has one use and belongs to a different live range, then omits the copy for
+the tie it broke. An allocator-generated Split is excluded: its single use can
+hide a longer-lived value, and commuting onto it can undo an earlier repair.
+The existing coloring retry rebuilds live ranges. There is no new whole-IR pass
+or analysis structure. The machine's `commutes()` hook covers Add, Mul, Xor,
+And, and Or (and commutative floating instructions).
+
+The String score is now 21: each hash body costs 9 and the allocator costs 3.
+The three x86 expectations are updated to 21. A single regression exercises
+all five integer operators in both machine operand orders, checks register
+constraints, and encodes the results. Existing testFlags2 guards against the
+Split-operand regression.
+
+## Incremental Iter setup
+
+Iter no longer resets and requeues the whole graph after parsing. `Node.init()`
+queues initialized nodes, and Calls queue themselves so Iter can link targets
+loaded after the call was parsed. Iter seeds its new global escape summary,
+then drains the worklist. Import loading still resets the newly loaded nodes
+and repairs affected neighbors.
+
+Removing the sweep also exposed an unused receiver left behind when field
+lookup switches from an instance to its class. That path now releases the keep
+and kills the receiver if unused.
+
+FldOffNode's base is data in slot 1. Slot 0 is reserved for control; putting a
+data parameter there made inliner body discovery exclude an unresolved offset.
+The regression `CallEndTest.testUnresolvedOffsetInInlineBody` checks that the
+offset belongs to the function body even before its type resolves.
+
+## Type upgrades in Opto setup
+
+ParseAll still closes the named types and freezes the public interface after
+loading all units. It no longer walks the IR. Opto installs those closed types
+and resolves forward references during its existing reset walk. Setup runs no
+peepholes against the partly reset graph: resolved FRefs propagate their input
+type in SCCP and disappear during subsequent peepholes. Construction identities
+also wait until this setup has installed complete field declarations.
+
+That walk retains Returns while call edges are rebuilt, gathers unresolved
+numeric modes, and records dependencies for cleanup after SCCP. Type upgrades
+also register cleanup dependencies, even if the eventual value type is unchanged.
+Old Iter types are snapshots for change detection, not upper bounds after
+recursive declarations have been closed. SCCP still asserts monotone falling
+types and a complete worklist within its own solve.
+
+The move exposed assumptions formerly hidden by early upgrades. Resolving an
+already known class name must retain its known fields instead of replacing them
+with an empty placeholder. A Load must not forward a stored pointer whose open
+shape is weaker than the Load's computed type. Escape must retain escapes from
+the actual private value before narrowing it to its declared field type, so
+selecting a Store out of a MemMerge does not introduce new escapes.
+
+An unknown return or memory effect carries FULL escape sets. StopCU and
+GlobalEscape preserve that bound: narrowing it to public names would omit
+private objects and callbacks that a subsequent load-forwarding rewrite can
+reveal. Both summaries recompute from their inputs without retaining previous
+escape sets. Refining a value removes possibilities; it never adds escapes
+outside the old bound. IterPeeps has no special summary handling, and its
+worklist assertion requires every node's type to lift, including queued nodes.
+`testUnknownEscapesRefineToPrivateValues` checks both escape sinks;
+`testPrivateRecursiveReturn` covers cyclic private objects and field loads.
+
+The ordinary dependency mechanism also covers signature refinement and lazy
+imports. FunNode depends on its Return when refining its signature. Calls
+depend on their target FunNode; while a target is absent, they depend on Start
+and retry when a linker mapping is installed. Loading bodies and adding call
+edges still use `Iter.repair` for graph changes, not for value refinement.
+
+SCCP's Start linking honors FULL for private targets too. Incoming arguments
+are not themselves outgoing escapes; Stores, Returns, and external-call
+arguments describe those effects. Constant singleton field slices carry the
+stored value's escapes, not unrelated bulk-memory escapes frozen at the time
+of folding. This preserves precision for local recursive callbacks.
+
+As bounds sharpen after SCCP, FunNode removes unknown callers even if concrete
+callers remain. Called initializers retain their Return through StopCU because
+CLInit has no Return edge. Removing an uncalled function queues the affected
+Stops, and compilation-unit wrappers retain distinct identities even when
+empty. These are worklist notifications and ownership rules, not extra passes.
+
+### Remaining precision issue with FULL escapes
+
+The monotone bounds expose a feedback problem in the existing grand cycle.
+In `Chapter12Test.testPrivateRecursiveReturn`, SCCP sees BOTTOM for the final
+load chain and therefore FULL escapes. These feed through Start's memory into
+the function memory results. Post-SCCP forwarding resolves the return to `_B`,
+but that memory cycle remains at MemBot and keeps private constructors callable
+by unknown callers. This is conservative, but loses the previous dead-function
+elimination. It does not justify narrowing BOTTOM to public names or adding
+the IterPeeps widening repair back.
+
+For this example, the root Return goes from `[Ctrl, MemBot, Bot]` after Opto to
+`[Ctrl, MemBot, *_B]` after TypeCheck; Stop's memory remains MemBot. The remaining
+design question is how to avoid this feedback while retaining conservative
+bounds and the existing pass budget. The current experiment leaves the printed
+IR expectations unchanged so they continue to expose this loss of precision.
+
+The source-validity audit separates these cases from Chapter10 `testIfOrNull`.
+That test originally returned the nullable pointer itself. A historical edit
+changed it to `return bar.a;` without adding TypeCheck, leaving a real null
+error hidden behind a printed-IR assertion. It now again returns `bar`, runs
+through TypeCheck, and passes with `return Phi(Region,null,Test._Bar);`.
+
+All four remaining programs pass TypeCheck. The Chapter12 recursive-object
+tests have one pending bad Load after Opto; forwarding eliminates every Load
+and resolves the result to `17`, `_B`, or `_R`, while escape memory stays MemBot.
+
+The two Chapter17b forward-type tests now pass their original IR expectations
+and explicitly run TypeCheck. Their undeclared `B` is an opaque external type,
+not an arbitrary unknown local value. EscapeNode uses the frozen public
+interface as its fallback for that type, preserving escapes already present
+in its private and public memory inputs. This applies to both the actual private
+value and the declared field bound: in `testForward1`, the private array element
+type is already `B?`. Bounding only FIDXs is insufficient; FULL aliases expose
+private class fields, which then expose their constructor FIDXs.
+
+CodeGen caches the fallback when ParseAll freezes the public interface. It
+returns FULL until Opto's existing reset walk installs the closed types into
+the IR. Only then does an open `_fref` reliably denote an external object.
+Ordinary open local structs, `TypeScalar.BOT`, and abstract pointers still
+require FULL. No new pass or IterPeeps hook is involved.
+
+### Escape-bound audit
+
+All Chapter29 `escapesFrom`, `XInt.FULL`, and `TypeMem.BOT` uses were reviewed,
+including the equivalent `escapes` helper and indirect `makePrivate` uses.
+The following remaining sites warrant work; they have not been narrowed here:
+
+* **StoreNode's three `escapesFrom(val)` paths.** The singleton constant,
+ private instance, and ordinary aliasing paths still treat an opaque external
+ value as FULL. A concrete example is:
+
+ ```simple
+ struct _A { B? !b; new _A = { B? p -> b=p; }; };
+ val wrap = { B? b -> return new _A(b); };
+ return wrap;
+ ```
+
+ This passes TypeCheck, but the surviving Stores of `b` have FULL FIDX and
+ alias sets and keep an extra `_A` allocator. Do not mechanically substitute
+ the public default: singleton slices deliberately discard unrelated incoming
+ memory escapes. A narrower opaque bound must retain private values previously
+ given to external code, without freezing unrelated transient escapes into
+ singleton constants. Ordinary aliasing Stores already retain incoming sets.
+
+* **StopCU return values, CallNode external arguments, and SummaryNode arguments.**
+ These use `escapes`, which currently duplicates the unbounded `escapesFrom`.
+ `val identity = { B? b -> return b; }; return identity;` passes TypeCheck but
+ gives StopCU MemBot. Likewise,
+ `val send = { B? b -> { B? -> int } retain = "C"; return retain(b); }; return send;`
+ gives the native CallEnd MemBot. These are concrete instances of the same
+ opaque-type precision loss. A future change can share the bounded helper,
+ while retaining incoming/returned memory, explicit private arguments, and
+ imported-summary effects. StopCU's public function seed and `escapesAliases`
+ class seed describe known declarations; GlobalEscape's `escapes` calls merely
+ union already computed TypeMem sets.
+
+* **CLInitNode's possible-initialization result.** Even a known initializer
+ yields TypeMem.BOT until its ready flag is true; only the initialized flag's
+ slice is precise. This loses both memory-content and escape precision.
+ CLInit has an analysis call edge but no Return edge supplying callee effects.
+ Improving it needs a dependency on the initializer's effects, including
+ recursive dependencies. The public interface alone is not a safe cap: that
+ initializer can publish private callbacks. Its unresolved-flag BOT fallback
+ is also conservative, but normally disappears when the receiver resolves.
+
+* **CallEndNode's unknown or unlinked target result.** Both return TypeMem.BOT.
+ The unlinked case covers invalid calls and transient missing edges; SCCP links
+ valid finite targets before computing CallEnd. Truly unknown targets can be
+ local private functions as well as external functions, so a public-only cap
+ would be wrong. Native calls already use CallNode's argument/memory effects.
+ Any refinement must distinguish target provenance and retain explicit private
+ arguments; it is not the same small fix as an opaque external object.
+
+The other broad defaults appear justified:
+
+* TypeMem's generic BOTTOM/scalar/pointer cases and TypeStruct's unknown scalar
+ fields must include private callbacks that later value refinement can reveal.
+ Private-memory declarations/Phis use `makePrivate(BOTTOM)` for the same reason.
+ MemMerge's `makePrivate(struct)` describes actual fields, not an external
+ opaque object.
+* Parser/function memory declarations, initial node types, TypeTuple.STATE,
+ and low-family error fallbacks in Store, MemMerge, BulkMemPhi, Start, and
+ TypeTuple need worst-case memory. CallNode and SummaryNode likewise cannot
+ promise bounded effects when their inputs are still arbitrary low values.
+* Iter's import reset gives newly loaded nonconstant memory FULL before repairing
+ affected neighbors. Saved types belong to another caller context; constants
+ retain their own computed types. This is a safe pessimistic seed, though it
+ can contribute to the existing memory-cycle precision problem.
+* TypeFunPtr's FULL signatures and TypeRPC.BOT describe unknown function/return
+ targets. Serialization/remapping preserves the FULL sentinel; type gathering
+ exercises it; LoopNode's `isa(TypeMem.BOT)` is a type-family check. These do
+ not independently manufacture an escape effect.
+
+Ignoring assertions, the remaining whole-node walks through TypeCheck are
+Opto's setup/reset and TypeCheck's error scan. Class-init checking separately
+scans function control flow and runs SCC over the resulting dependency graph.
+
+## Validation
+
+Chapter29's latest run tested 594 active cases with assertions: 590 passed and
+four printed-IR comparisons failed. `make -k tests` ran raw0 (400/404), raw1
+(38), standalone (13), system (29), and fuzzer (1); the aggregate group (109)
+was run separately because Make skips it after a prerequisite failure.
+The remaining failures retain extra private constructors: Chapter12 `testCoRecur`,
+`testCoRecur2`, `testCoRecur3`, and `testPrivateRecursiveReturn`. Both Chapter17b
+forward tests pass unchanged goldens. TypeTest.testOpaqueExternalEscapes checks
+both fallback sets, preservation of prior private escapes, and FULL for ordinary
+unknowns. System, spill, import, callback, and constructor tests pass. The only
+golden changed during the earlier source-validity audit belongs to the restored
+`return bar;` source; none were changed to accept extra functions.
+
+The offset regression fails with the old slot-0 layout. Constructor and
+boundary tests pass, including both missed rejections and the prior NPE.
+Printed-IR expectations omit the dead private-helper MemTop Returns; no
+rejection expectations were relaxed. The three String spill checks now expect
+21 on x86, with the same live functions and inlining decisions.
diff --git a/docs/chapter30/escape-flow.md b/docs/chapter30/escape-flow.md
new file mode 100644
index 00000000..8ad49f15
--- /dev/null
+++ b/docs/chapter30/escape-flow.md
@@ -0,0 +1,147 @@
+# Outside-world escape flow
+
+The `escape-flow` branch developed this design in Chapter29 and backported it
+to Chapters 28a/28b. Chapter28a uses `LocalIds` and its existing initializer
+root/lowering protocol; Chapter28b adds saved summaries and cofinite identity
+remapping. Neither port imports Chapter29's deferred-construction model.
+
+## The cycle
+
+The outside world's access to local objects and functions is a value in the
+Start/Stop cycle. Public declarations are not that value. The frozen public
+interface inventory has been removed; public-method and public-alias tests
+check the converged outside escape sets directly.
+
+At Opto setup, the outer Start receives a constant foreign-identity seed:
+
+```
+foreign identities = FULL minus identities owned by this compilation
+outside identities = foreign identities union discovered local escapes
+```
+
+Known native function and storage identities are foreign too. The infinite
+tail permits future foreign functions even when no local callback has escaped.
+The finite exclusions include registered imported identities, including bodies
+represented by summaries. This scans identity tables, not the IR. Opto's
+existing reset/worklist solve discovers the local escapes.
+
+StartCU includes the shared outside state. Infinite FIDX sets must retain their
+exclusions: `Opto.linkStart` checks each local function's membership rather than
+treating every cofinite set as FULL. Serialization remaps a cofinite set by
+remapping its finite exclusions and complementing again.
+
+Before Opto, outside queries remain pessimistic. During SCCP, a consumer can
+read the foreign seed before Start has run: otherwise a temporary finite set
+can turn a generic foreign callback into an unrelated local function with the
+same arity. No previous escape result is accumulated to repair value flow.
+
+## Consumers and dependencies
+
+* Memory and callback defaults for unknown callers use Start's memory
+ projection. Parm applies its declared callback signature to the escaping
+ FIDX set. Defaults whose values do not depend on escapes remain constants.
+* Unknown/native CallEnd results use the outside state and the current call's
+ exposed arguments. Callback results are bounded by their declared signature
+ and the outside FIDX set. Explicit linked Return effects are retained.
+* CallEnd registers a direct dependency on Start. Depending only on Call is
+ insufficient: Call can stay CONTROL while the outside set changes. This
+ first implementation uses `addDepForwards`; it does not change CallEnd's
+ input layout or introduce a synthetic Fun/Return pair.
+* Infinite calls use the outside continuation. Escaped local functions remain
+ callable through Start, and their effects return through Stop. Attaching
+ every same-arity body to an infinite call is incorrect: argument/result
+ families may differ despite the equal arity.
+* Outgoing Call argument exposure is independent of CallEnd, including a call
+ that never returns. The shared fixed point also permits immediate callback
+ round trips and reentrant calls.
+* Opaque external values in Escape, Store, Return, and imported-summary paths
+ use the current outside/input-memory bound. Explicit private values and
+ already accumulated escapes remain included. Arbitrary local BOTTOM values
+ still require FULL.
+* Function-valued Loads use their memory input's FIDX bound, joined with the
+ field declaration. They do not independently manufacture an escape set.
+
+CLInit was another unconditional producer of FULL. During Opto it now depends
+on the known initializer's Return and merges those effects with incoming
+memory. It waits for live memory before publishing effects. Lowering computes
+the replacement CallEnd's linked type before exposing its projections, so a
+temporary unlinked MemBot cannot widen the already analyzed caller.
+
+## Why the recursive tests needed local precision too
+
+All four former Chapter12 printed-IR failures pass their original expectations.
+The problem was not solely an outside-call default:
+
+1. Convert widened a non-null pointer to a nullable declaration until its later
+ identity rewrite. It now preserves non-nullness in compute. Preserving the
+ entire source shape conditionally failed the existing monotonicity test;
+ shape and access still follow the declaration.
+2. A Load could wait until post-SCCP Store forwarding to recover that non-null
+ pointer. In the meantime the next dereference produced BOTTOM, which fed
+ FULL around the memory cycle and revived private constructors.
+3. Forwarded addresses, resolved field aliases, and already-bypassed CLInit
+ projections were also waiting for graph rewrites.
+
+Load's existing forwarding search now supplies pointer value facts during
+compute, without constructing or rewriting nodes. It can recognize a stored
+address through a valid pointer conversion or a proven forwarded Load, use a
+resolved named field before layout, and follow an existing CLInit bypass.
+The same dependency mechanism reruns the search when its evidence changes.
+Nullable/incomplete access checks still precede this sharpening; arbitrary
+memory Phis and unknown stores remain barriers. This adds local queries, not a
+new whole-program pass, analysis map, or second IR.
+
+## Hidden class receivers
+
+A class function's hidden `#self` defaults to its owning class singleton.
+Seeding that input with TypePtr.PTR used to give FULL escapes when forwarded
+to an unknown callback. For example:
+
+```simple
+val _hidden_noInline = { int n -> return n+2; };
+val apply_noInline = { {int->int} f,int n -> return f(n); };
+return apply_noInline;
+```
+
+`FunNode.defaultArg` now supplies the Test class singleton for this receiver,
+both during parsing and when Opto reconnects unknown callers. The callback
+parameter still reads Start dynamically. The printed-IR regression verifies
+that only `<clinit>` and `apply_noInline` escape; `_hidden_noInline` stays private.
+
+The function signature retains TypePtr.PTR in this slot. The backends use it
+to identify the hidden ABI argument, and callback annotations must not bind
+the receiver to the class where the annotation appears. Explicit calls still
+supply their actual receiver; instance methods keep their declared object
+type. This sharpens the default input, not every abstract pointer: a local
+merge of a private object and private callback can also have an abstract
+pointer type, so capping arbitrary TypePtr values at outside escapes is unsound.
+
+## Remaining design question
+
+Late imported identities also deserve further precision work: identities not
+registered when Opto starts lie in the foreign tail. Existing import tests
+exercise correctness, but this can remain conservative when bodies arrive
+later. This limitation is not repaired by resetting the whole IR.
+
+## Validation
+
+The combined `make -k tests CHAPTERS="chapter28a chapter28b chapter29" CTAGS=`
+run passes with assertions: 538 active tests in Chapter28a, 578 in Chapter28b,
+and 601 in Chapter29. Shared printer and ISA checks pass too. The log is saved
+under ignored `build/escape-backport-final.log`.
+
+`EscapeFlowTest` covers opaque wrappers with three worklist seeds, foreign
+callback parameters, private callbacks passed to and returned from native code,
+and a CallEnd dependency that responds when only Start's escape set changes.
+It also checks that public allocators escape while their private-memory helpers
+do not. Class-init tests cover unknown callback cycles; saved-library tests in
+28b/29 verify private-function exclusions survive identity remapping.
+The existing suites cover nonreturning native calls, class-init cycles,
+constructor rejection, serialization, imported callbacks, and three backends.
+The four recursive-class examples also pass a separate 16-case worklist-seed
+check. Backport IR expectations now retain public allocator Returns instead of
+private-memory helper Returns. The String allocation/encoding/export fixtures
+in 28a/28b/29 now declare `_hashCodeString` before the public `hashCode` wrapper,
+so the implicit return exposes the wrapper. Their measured spill expectations
+are 12/8/6 (x86/RISC-V/ARM); native x86 export expects 12. The reordered fixture
+also measures 8 on RISC-V before the backport, versus the old order's 6.
diff --git a/src/test/smp/examples/A_helloWorld.smp b/docs/chapter30/examples/A_helloWorld.smp
index 4ed6abdf..4ed6abdf 100644
--- a/src/test/smp/examples/A_helloWorld.smp
+++ b/docs/chapter30/examples/A_helloWorld.smp
diff --git a/src/test/smp/examples/BubbleSort.smp b/docs/chapter30/examples/BubbleSort.smp
index ca6906b5..6bdb13a9 100644
--- a/src/test/smp/examples/BubbleSort.smp
+++ b/docs/chapter30/examples/BubbleSort.smp
@@ -8,9 +8,11 @@ val sort = { !sys.aryi64 nums ->
sys.aryi64?! more = nums;
while( more ) {
more = null;
- for( var i = 0; i < nums.len-1; i++ )
- if( nums.buf[i] > nums.buf[i+1] )
- more = nums.swap(i,i+1);
+ for( u32 i = 0; i < nums.len-1; i++ )
+ if( nums.buf[i] > nums.buf[i+1] ) {
+ u32 next = i+1;
+ more = nums.swap(i,next);
+ }
}
};
diff --git a/src/test/smp/examples/Capitalize.smp b/docs/chapter30/examples/Capitalize.smp
index 8e31c18f..00fdd481 100644
--- a/src/test/smp/examples/Capitalize.smp
+++ b/docs/chapter30/examples/Capitalize.smp
@@ -14,7 +14,8 @@ val main = { u8[~][] args ->
u8[~] input = args[1];
var output = new u8[input# + 1];
- sys.ary.copy(output,0,input,0,input#);
+ u32 len = input#;
+ sys.ary.copy(output,0,input,0,len);
output[0] = sys.char.toupper(input[0]);
output[input#] = '\n';
sys.io.p(output);
diff --git a/src/test/smp/examples/Dijkstra.smp b/docs/chapter30/examples/Dijkstra.smp
index fd595eb5..768bdd08 100644
--- a/src/test/smp/examples/Dijkstra.smp
+++ b/docs/chapter30/examples/Dijkstra.smp
@@ -26,7 +26,8 @@ val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst ->
if( nearest == dst ) return best;
visited.set(nearest);
for( int node=0; node<nodes; node++ ) {
- i64 weight = matrix.get(nearest*nodes + node);
+ u32 index=nearest*nodes + node;
+ i64 weight = matrix.get(index);
if( weight > 0 && !visited.get(node) && best+weight < dist[node] )
dist[node] = best+weight;
}
@@ -49,7 +50,7 @@ val main = { u8[~][] args ->
if( !matrix ) return usage();
if( !matrix.len ) return usage();
// All numbers are positive
- for( int i=0; i<matrix.len; i++ )
+ for( u32 i=0; i<matrix.len; i++ )
if( matrix.get(i) < 0 ) return usage();
// Matrix is square
int nodes = 0;
@@ -63,7 +64,8 @@ val main = { u8[~][] args ->
if( src < 0 || src >= nodes || dst < 0 || dst >= nodes )
return usage(); // Source and destination land within the graph
- i64 cost = shortestPath(matrix,nodes,src,dst);
+ int source=src, destination=dst;
+ i64 cost = shortestPath(matrix,nodes,source,destination);
if( cost >= 0 ) {
new sys.aryu8().num(cost).add('\n').write(1);
return 0;
diff --git a/src/test/smp/examples/FileIO.smp b/docs/chapter30/examples/FileIO.smp
index e48d435b..59b3159e 100644
--- a/src/test/smp/examples/FileIO.smp
+++ b/docs/chapter30/examples/FileIO.smp
@@ -15,7 +15,8 @@ val main = { u8[~][] args ->
i32 fd = sys.io.create(FILE);
if( fd == -1 ) return _error();
// Write
- int wrote = sys.io.writeOff(fd,str.buf,0,str.len);
+ u32 len = str.len;
+ int wrote = sys.io.writeOff(fd,str.buf,0,len);
if( wrote == -1 ) return _error();
if( wrote != str.len ) return _error2(-2);
// Close
@@ -26,12 +27,13 @@ val main = { u8[~][] args ->
if( fd2 == -1 ) return _error();
// Read
var got = sys.io.read(fd2);
+ if( !got ) return _error();
if( got# != str.len ) return _error2(-3);
// Close
if( sys.io.close(fd2) != 0 ) return _error();
// Check contents are equal
- if( sys.ary.cmp(got,0,str.buf,0,str.len) )
+ if( sys.ary.cmp(got,0,str.buf,0,len) )
return _error2(-4);
// Remove
diff --git a/docs/chapter30/examples/README.md b/docs/chapter30/examples/README.md
new file mode 100644
index 00000000..767cdc3e
--- /dev/null
+++ b/docs/chapter30/examples/README.md
@@ -0,0 +1,71 @@
+# Runnable examples
+
+These programs live beside the documentation so readers can find and run them.
+Their native regression tests still run as part of `make tests`.
+
+From `chapter30`, build the tic-tac-toe executable (without starting it):
+
+```sh
+make lib tictactoe CTAGS=
+```
+
+Run the Windows build from PowerShell:
+
+```powershell
+.\build\examples\x86_64_v2_win64\TicTacToe.exe
+```
+
+Or from Cygwin:
+
+```sh
+./build/examples/x86_64_v2_win64/TicTacToe.exe
+```
+
+The Windows executable uses Cygwin's runtime; its `bin` directory must be on
+`PATH` (as it is when building with Cygwin GCC).
+
+For a native Linux build, run these commands from this same `chapter30`
+directory in WSL. Separate Java and native output directories let the Windows
+and Linux builds coexist:
+
+```sh
+make lib tictactoe CLZDIR=build/classes-linux CTAGS=
+./build/examples/x86_64_v2_SystemV/TicTacToe
+```
+
+From PowerShell, prefix either command with `wsl -d Ubuntu-24.04 --`.
+On a standalone Linux checkout, `CLZDIR` can be omitted.
+The compiler's `--link` option produces an executable without running it;
+`--norun` continues to produce object files only.
+
+`TicTacToe.smp` currently prints the welcome, square numbering, and empty board,
+then waits for a move. Press **1–9** without Enter to place X, or
+**q**, Escape, Ctrl-C, or Ctrl-D to quit. Enter is silently ignored; other
+invalid keys and occupied squares prompt again without taking a turn. The board
+is printed after each player and CPU move; only accepted move digits are echoed.
+Play continues until someone wins or the board is
+full. The CPU takes an immediate win, blocks an immediate loss, then prefers
+the center, corners, and edges.
+
+Other examples are Hello World (`A_helloWorld.smp`), `BubbleSort.smp`,
+`Capitalize.smp`, `Dijkstra.smp`, and `FileIO.smp`. The latter creates, checks,
+and removes `output.txt` in the working directory.
+
+## Single-keystroke input
+
+`sys.io.getch()` blocks for one byte without terminal editing or echo. It returns
+0–255 for a byte, or -1 for EOF or an error. On -1, `sys.libc.errno()` is
+`sys.libc.EOF` (-1) for end-of-input, otherwise the OS error code.
+Terminal Ctrl-C and Ctrl-D are bytes 3
+and 4, so the application decides how to handle them. Unicode and function keys
+may produce multiple bytes; this is an ASCII-oriented byte API, not a key-event
+decoder.
+
+The small C runtime adapter uses [POSIX terminal modes](https://man7.org/linux/man-pages/man3/termios.3.html),
+including Cygwin on Windows. It restores the saved mode before returning,
+including after an interrupted read. Files and pipes use an ordinary one-byte
+read. `make test CLASS=TerminalInputTest` runs the Java regression tests for
+redirected input, errors, prompt output, Enter, and Escape. Check terminal
+editing, echo, and mode restoration by running the executable in a terminal.
+`make test CLASS=TicTacToeTest` checks complete games, all eight winning lines,
+draws, occupied squares, and CPU blocking.
diff --git a/docs/chapter30/examples/TicTacToe.smp b/docs/chapter30/examples/TicTacToe.smp
new file mode 100644
index 00000000..a710e440
--- /dev/null
+++ b/docs/chapter30/examples/TicTacToe.smp
@@ -0,0 +1,133 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+val main = { u8[~][] args ->
+ sys.io.p("Tic-tac-toe!\nYou are X; the CPU is O. You go first.\n");
+ sys.io.p("Press a square's number (no Enter needed), or q/Esc to quit.\n");
+ sys.io.p("Squares:\n 1 | 2 | 3 \n---+---+---\n 4 | 5 | 6 \n---+---+---\n 7 | 8 | 9 \n\n");
+ val board = new u8[9];
+ for( int i=0; i<board#; i++ ) board[i]=' ';
+ // Draw empty board
+ _draw(board);
+
+ while( true ) {
+ sys.io.p("Your move (1-9): ");
+ // Valid user move 1-9, or 0 for quitting
+ int square = _validMove(board) - 1;
+ if( square < 0 ) return 0;
+ // Set user choice; print board
+ board[square] = 'X';
+ sys.io.p("\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ // Compute CPU move
+ int reply = _cpu(board);
+ // Set CPU choice; print board
+ board[reply]='O';
+ sys.io.p("CPU plays %(reply+1).\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ }
+};
+
+// Return with a valid user move 1-9, or 0 to exit
+val _validMove = { u8[~] board ->
+ while( true ) {
+ int key = sys.io.getch();
+ if( key == -1 && sys.libc.errno() != sys.libc.EOF ) {
+ sys.io.err("\nUnable to read the keyboard.\n");
+ return 0;
+ }
+ // Various quitting conditions
+ if( key == -1 || key == 'q' || key == 'Q' || key == 27 || key == 3 || key == 4 ) {
+ sys.io.p("\nGoodbye!\n");
+ return 0;
+ }
+ // Ignore CR/LF
+ if( key == '\r' || key == '\n' ) continue;
+
+ // Valid choice
+ if( key < '1' || key > '9' ) {
+ sys.io.p("\nChoose a square from 1 to 9: ");
+ continue;
+ }
+ if( board[key-'1'] != ' ' ) {
+ sys.io.p("\nThat square is occupied. Your move (1-9): ");
+ continue;
+ }
+ sys.io.p("%c(key)");
+ return key-'0';
+ }
+};
+
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ for( int i=0; i<3; i++ ) {
+ int row = i*3;
+ if( board[row]!=' ' && board[row]==board[row+1] && board[row]==board[row+2] )
+ return board[row];
+ if( board[i]!=' ' && board[i]==board[i+3] && board[i]==board[i+6] )
+ return board[i];
+ }
+ if( board[4]!=' ' ) {
+ if( board[0]==board[4] && board[4]==board[8] ) return board[4];
+ if( board[2]==board[4] && board[4]==board[6] ) return board[4];
+ }
+ return 0;
+};
+
+val _finished = { u8[~] board ->
+ int winner = _winner(board);
+ if( winner=='X' ) { sys.io.p("You win!\n"); return true; }
+ if( winner=='O' ) { sys.io.p("CPU wins!\n"); return true; }
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return false;
+ sys.io.p("It's a draw!\n");
+ return true;
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i=0; i<board#; i++ ) {
+ if( board[i]==' ' ) {
+ board[i]=mark;
+ bool wins = _winner(board)==mark;
+ board[i]=' '; // Restore board
+ if( wins ) return i;
+ }
+ }
+ return -1;
+};
+
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win>=0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block>=0 ) return block;
+ // If center is open, take it
+ if( board[4]==' ' ) return 4;
+ // If a corner is open, take it
+ for( int i=0; i<board#; i+=2 )
+ if( board[i]==' ' ) return i;
+ // Take whatever is available
+ for( int i=0; i<board#; i++ )
+ if( board[i]==' ' ) return i;
+ return -1;
+};
+
+val _draw = { u8[~] board ->
+ val out = new sys.aryu8();
+ for( int row=0; row<3; row++ ) {
+ for( int col=0; col<3; col++ ) {
+ out.add(' ').add(board[row*3+col]).add(' ');
+ if( col<2 ) out.add('|');
+ }
+ out.add('\n');
+ if( row<2 ) out.addAry("---+---+---\n");
+ }
+ out.write(sys.libc.STDOUT);
+};
diff --git a/src/main/c/simple_crt.c b/src/main/c/simple_crt.c
index 8656a3cc..438ae5f9 100644
--- a/src/main/c/simple_crt.c
+++ b/src/main/c/simple_crt.c
@@ -1,58 +1,106 @@
#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
+#include <termios.h>
+#include <unistd.h>
// errno may be thread-local or a macro calling a platform accessor.
int32_t simple_errno(void) { return errno; }
+int32_t simple_set_errno(int32_t error) { errno = error; return -1; }
+// Export platform errno constants; the buffering and retry loops are Simple.
+// C identifiers avoid the macros; linker names match the Simple bindings.
+int32_t simple_EINTR __asm__("EINTR") = EINTR;
+int32_t simple_EIO __asm__("EIO") = EIO;
+int32_t simple_EINVAL __asm__("EINVAL") = EINVAL;
+int32_t simple_EOVERFLOW __asm__("EOVERFLOW") = EOVERFLOW;
-// Native process arguments converted to ordinary Simple array layouts.
-// The outer array is mutable; every contained byte array is published as
-// immutable and non-null to Simple code.
+// Read one stdin byte, without terminal line editing or echo. POSIX terminals
+// include Cygwin consoles; redirected files/pipes need no mode change.
+// Return 0..255, or -1 with errno set to EOF or an OS error. Control characters,
+// including Ctrl-C, are returned as bytes so the mode is restored first.
+int32_t simple_getch(void) {
+ struct termios saved, mode;
+ int terminal = tcgetattr(STDIN_FILENO, &saved) == 0;
+ if (!terminal && errno != ENOTTY)
+ return -1;
+ if (terminal) {
+ mode = saved;
+ mode.c_lflag &= ~(ICANON | ECHO | ECHONL | ISIG | IEXTEN);
+ mode.c_iflag &= ~IXON;
+ mode.c_cc[VMIN] = 1;
+ mode.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &mode) != 0)
+ return -1;
+ }
+ unsigned char byte;
+ ssize_t count = read(STDIN_FILENO, &byte, 1);
+ int read_errno = errno;
+ if (terminal) {
+ int restored;
+ do {
+ restored = tcsetattr(STDIN_FILENO, TCSANOW, &saved);
+ } while (restored != 0 && errno == EINTR);
+ if (restored != 0)
+ return -1;
+ }
+ if (count < 0) {
+ errno = read_errno;
+ return -1;
+ }
+ if (count == 0) {
+ errno = EOF;
+ return -1;
+ }
+ return byte;
+}
+
+// Simple byte-array layout, used only by the floating-point formatter.
typedef struct SimpleBytes {
uint32_t len;
unsigned char bytes[];
} SimpleBytes;
-typedef struct SimpleArgs {
- uint32_t len;
- uint32_t pad;
- SimpleBytes *args[];
-} SimpleArgs;
+// Binary floating-point conversion is the only native formatting primitive.
+// Buffering, integer conversion, string precision and padding live in sys.text.
+SimpleBytes *simple_text_float(double value, int64_t spec,
+ int64_t width, int64_t precision) {
+ char kind = spec & 255;
+ if (!kind) { kind = 'g'; precision = 17; }
+ if (!strchr("fFeEgGaA", kind) || (spec & ~8191LL) ||
+ width < 0 || width > INT32_MAX || precision < -1 || precision > INT32_MAX)
+ abort();
+ char fmt[64], *p = fmt;
+ *p++ = '%';
+ const char *flags = "-+ #0";
+ for (int i=0; i<5; i++) if (spec & (256LL<<i)) *p++ = flags[i];
+ if (width) p += sprintf(p, "%d", (int)width);
+ if (precision >= 0) p += sprintf(p, ".%d", (int)precision);
+ *p++ = kind;
+ *p = 0;
+ int len = snprintf(NULL, 0, fmt, value);
+ if (len < 0) abort();
+ SimpleBytes *str = malloc(sizeof(*str) + (size_t)len + 1);
+ if (!str) abort();
+ str->len = (uint32_t)len;
+ snprintf((char *)str->bytes, (size_t)len + 1, fmt, value);
+ return str;
+}
+// Simple does not yet dereference raw native pointers. Keep just the argv
+// element access here; allocation and array construction live in sys/rt/simple_args.smp.
+const char *simple_argv(char **argv, int64_t index) {
+ return argv[index];
+}
__attribute__((CALL_CONV))
-extern int64_t simple_main(SimpleArgs *args);
-
-static SimpleArgs *simple_args(int argc, char **argv) {
- if (argc < 0 || (uint64_t)argc > UINT32_MAX)
- return NULL;
-
- SimpleArgs *args = calloc(1, sizeof(*args) + (size_t)argc * sizeof(args->args[0]));
- if (args == NULL)
- return NULL;
- args->len = (uint32_t)argc;
-
- for (int i = 0; i < argc; i++) {
- size_t len = strlen(argv[i]);
- if (len > UINT32_MAX) {
- free(args);
- return NULL;
- }
- SimpleBytes *str = malloc(sizeof(*str) + len);
- if (str == NULL) {
- for (int j = 0; j < i; j++) free(args->args[j]);
- free(args);
- return NULL;
- }
- str->len = (uint32_t)len;
- memcpy(str->bytes, argv[i], len);
- args->args[i] = str;
- }
- return args;
-}
+extern void *simple_args(int64_t argc, char **argv);
+__attribute__((CALL_CONV))
+extern int64_t simple_main(void *args);
int main(int argc, char **argv) {
- SimpleArgs *args = simple_args(argc, argv);
+ // simple_args fills every slot before publishing this as u8[~][].
+ void *args = simple_args(argc, argv);
if (args == NULL)
return 1;
// The language-level hidden receiver is not part of the emitted ABI for
diff --git a/src/main/java/com/seaofnodes/simple/README.md b/src/main/java/com/seaofnodes/simple/README.md
index 613f66b4..af86f21e 100644
--- a/src/main/java/com/seaofnodes/simple/README.md
+++ b/src/main/java/com/seaofnodes/simple/README.md
@@ -2,7 +2,7 @@
| Directory | Contents |
| --- | --- |
-| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter29/29-grammar.md). |
+| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter30/30-grammar.md). |
| `node/` | Sea-of-Nodes IR nodes: graph edges, type computation, and local peephole rewrites. |
| `node/cpus/` | Machine-specific nodes and instruction selection support for `arm`, `riscv`, and `x86_64_v2`. |
| `codegen/` | Compilation driver and passes: module loading, optimization, scheduling, register allocation, serialization, and machine-code emission. `IterPeeps` runs the peephole worklist. |
diff --git a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
index 0b314978..8d0237ea 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/BuildLRG.java
@@ -16,7 +16,7 @@ abstract public class BuildLRG {
// true if no hard-conflicts, although we still might not color.
public static boolean run(int round, RegAlloc alloc) {
FunNode fun = null;
- for( Node bb : alloc._code._cfg ) {
+ for( Node bb : alloc._cfg ) {
if( bb instanceof FunNode fun2 )
fun = fun2;
for( Node n : bb.outs() ) {
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java b/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
index 689ceceb..dbde2f8b 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Coalesce.java
@@ -13,7 +13,7 @@ abstract public class Coalesce {
// Walk all the splits, looking for coalesce chances
boolean progress = false;
- for( CFGNode bb : alloc._code._cfg ) {
+ for( CFGNode bb : alloc._cfg ) {
outer:
for( int j=0; j<bb.nOuts(); j++ ) {
Node n = bb.out(j);
diff --git a/src/main/java/com/seaofnodes/simple/codegen/IFG.java b/src/main/java/com/seaofnodes/simple/codegen/IFG.java
index d15e66da..6ad7c0ad 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/IFG.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/IFG.java
@@ -72,7 +72,7 @@ abstract public class IFG {
WORK.clear();
// Last block has nothing live out
- for( CFGNode bb : alloc._code._cfg )
+ for( CFGNode bb : alloc._cfg )
if( bb.blockHead())
WORK.push( bb );
@@ -164,6 +164,10 @@ abstract public class IFG {
// Also record use-side spills for biased coloring.
// Then make all inputs live.
for( int i=1; i<n.nIns(); i++ ) {
+ // Metadata and scheduling dependencies do not consume registers.
+ // In particular, a static call's hidden class argument has no ABI
+ // location and must not keep its constant live across other calls.
+ if( ((MachNode)n).regmap(i)==null ) continue;
Node def = n.in(i);
if( def==null ) continue;
LRG lrg1 = alloc.lrg(def);
@@ -480,7 +484,7 @@ abstract public class IFG {
private static int functionArea(FunNode fun, RegAlloc alloc) {
int area=0;
- for( CFGNode bb : alloc._code._cfg )
+ for( CFGNode bb : alloc._cfg )
if( bb.fun()==fun )
area += bb.nOuts();
return area;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
index 5fc002a1..2595b4ad 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/InstSelect.java
@@ -85,13 +85,7 @@ class InstSelect {
}
// Produce a machine node from n; map it to flag as done so stops cycles.
- if( n instanceof MemOpNode mem && mem.off() instanceof ExternOffsetNode off ) {
- // External fields replace base+offset with symbol+0. Keep the ideal
- // declaring type on the address for machine load/store width selection.
- TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type;
- n._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern));
- n._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO));
- }
+ if( n instanceof MemOpNode mem ) Machine.externalAddress(mem);
map.put(n, x=_code._mach.instSelect(n) );
// Carry loop-tree and pre-order info across the ideal->mach transition
if( n instanceof CFGNode ncfg && x instanceof CFGNode xcfg ) {
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Machine.java b/src/main/java/com/seaofnodes/simple/codegen/Machine.java
index 3eee8fc3..6ccf6b19 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Machine.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Machine.java
@@ -3,6 +3,7 @@ package com.seaofnodes.simple.codegen;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeFunPtr;
import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeMemPtr;
abstract public class Machine {
// Human readable machine name. Something like "x86-64" or "arm" or "risc5"
@@ -46,6 +47,17 @@ abstract public class Machine {
// Instruction select from ideal nodes
public abstract Node instSelect( Node n );
+ // Lower external fields before selecting a memory operation, including
+ // loads absorbed into another instruction rather than selected separately.
+ public static void externalAddress(MemOpNode mem) {
+ if( mem.off() instanceof ExternOffsetNode off ) {
+ // Retain the declaring type for load/store width selection.
+ TypeMemPtr ptr = (TypeMemPtr)mem.ptr()._type;
+ mem._inputs.set(2,new ExternNode(TypeMemPtr.make((byte)2,ptr._obj,true),off._extern));
+ mem._inputs.set(3,ConstantNode.raw(TypeInteger.ZERO));
+ }
+ }
+
/** Stack Slot Numbering
RA gives all registers a number, starting at 0. Stack slots continue this
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
index 5f8a0ec0..3306ff8f 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
@@ -140,7 +140,7 @@ public abstract class ParseAll {
fref._name = TypeStruct.addClzPrefix(xcunit._cname);
cunit.addDep(xcunit);
} else {
- throw new RuntimeException("Undefined name '" + fref._name +"'");
+ throw fref.err();
}
} catch( IOException ioe ) {
throw new RuntimeException(ioe);
@@ -297,14 +297,6 @@ public abstract class ParseAll {
Parser.TYPES.put(cunit._cname,ptr._obj);
}
- // Symbol not found in source code (or we would not be here).
- // Search the module for the fref
- public static CompUnit findCompUnitOrThrow(CodeGen code, CompUnit cu, String symbol) {
- CompUnit cu2 = findCompUnit(code,cu,symbol);
- if( cu2 != null ) return cu2;
- throw new RuntimeException("Undefined name '" + symbol +"'");
- }
-
public static CompUnit findCompUnit(CodeGen code, CompUnit cu, String symbol) {
// Symbol not found in source code (or we would not be here).
// Search the module for the fref
@@ -328,8 +320,15 @@ public abstract class ParseAll {
// However if A/B contains "Y", the search would be for A.B.Y then A.Y both of
// which do not exist.
private static CompUnit findCUnitModule( CodeGen code, CompUnit cunit, String symbol ) throws IOException {
- if( cunit == null ) // Ran off top of module scope?
- return null; // Failed search
+ if( cunit == null ) {
+ // The module root is also a namespace. In particular, a root
+ // client can resolve sys.text from source, without installed .o's.
+ String fname=symbol.replace('.','/');
+ if( !new File(code._modDir+"/"+fname+".smp").exists() ) return null;
+ CompUnit root=makeCUnit(code,null,fname);
+ if( root._clz==null && WORK.find(root)==-1 ) enqueue(root);
+ return root;
+ }
// Check self
if( cunit._name.equals(symbol) )
diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
index f1aa18ef..3991cd48 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
@@ -54,6 +54,8 @@ public class RegAlloc {
// Top-level program graph structure
final CodeGen _code;
+ // Scheduled blocks of the function currently being allocated.
+ Ary<CFGNode> _cfg;
public int _spills, _spillScaled;
@@ -161,7 +163,7 @@ public class RegAlloc {
}
// -----------------------
- RegAlloc( CodeGen code ) { _code = code; }
+ RegAlloc( CodeGen code ) { _code = code; _cfg = code._cfg; }
public void regAlloc() {
// Insert callee-save registers
@@ -186,18 +188,41 @@ public class RegAlloc {
// Expensive assert: useful when chasing cross-function allocator edges.
//assert verifyFunctionLocalEdges();
- // Top driver: repeated rounds of coloring and splitting.
- byte round=0;
- while( !graphColor(round) ) {
- split(round);
- if( round >= 7 ) // Really expect to be done soon
- throw new IllegalStateException("Register allocation made no progress after eight rounds");
- round++;
+ // Each function owns its retry budget. Keep completed assignments while
+ // reusing the live-range and interference scratch space for the next.
+ IdentityHashMap<Node,LRG> colored = new IdentityHashMap<>();
+ _cfg = new Ary<>(CFGNode.class);
+ for( CFGNode bb : _code._cfg ) {
+ if( bb instanceof FunNode && !_cfg.isEmpty() ) {
+ allocateFunction();
+ colored.putAll(_lrgs);
+ _cfg.clear();
+ }
+ _cfg.add(bb);
}
+ if( !_cfg.isEmpty() ) {
+ allocateFunction();
+ colored.putAll(_lrgs);
+ }
+ _lrgs.clear();
+ _lrgs.putAll(colored);
+ _cfg = _code._cfg;
postColor(); // Remove no-op spills
_done = true;
}
+ private void allocateFunction() {
+ _deferredLoopSelf.clear();
+ for( byte round=0; !graphColor(round); round++ ) {
+ split(round);
+ if( round >= 7 ) {
+ FunNode fun = _cfg.at(0).fun();
+ throw new IllegalStateException("Register allocation failed after eight rounds in "+
+ (fun==null ? "<root>" : fun.label()));
+ }
+ }
+ }
+
boolean verifyFunctionLocalEdges() {
for( CFGNode bb : _code._cfg )
for( Node use : bb._outputs ) {
@@ -477,7 +502,10 @@ public class RegAlloc {
split.killOrdered();
}
for( int i=1; i<(deferred.containsKey(phi) ? 2 : phi.nIns()); i++ )
- if( i==1 || sharedPhiInput(phi,i) )
+ // Backedges may be joined indirectly through two-address
+ // chains, even when their input nodes are distinct. After
+ // the cold-only attempt, isolate those inputs as well.
+ if( i==1 || phi.region() instanceof LoopNode || sharedPhiInput(phi,i) )
insertBefore(phi,i,"use/self/phi",round,lrg);
}
// Split before two-address ops which extend the live range
@@ -653,9 +681,8 @@ public class RegAlloc {
return;
}
makeSplit(def,kind,round,lrg,umask).insertBefore(n, i);
- // Skip split-of-split same block
- if( skip && def instanceof SplitNode && cfg==def.cfg0() )
- n.in(i).setDefOrdered(1,def.in(1));
+ // Keep the def-side transfer: a kill between the two copies may be
+ // exactly why this range split. Coalescing can join them when safe.
}
void insertBefore(Node n, int i, String kind, byte round, LRG lrg) {
insertBefore(n,i,kind,round,lrg,true);
diff --git a/src/main/java/com/seaofnodes/simple/codegen/SourceError.java b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
index f2f46146..5be0585f 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/SourceError.java
@@ -16,6 +16,10 @@ public abstract class SourceError {
source=cu._src!=null ? cu._src.getBytes(StandardCharsets.UTF_8)
: file==null ? null : Files.readAllBytes(Path.of(file));
} catch( IOException ex ) { source=null; }
+ return format(name,source,pos,message);
+ }
+
+ public static String format(String name, byte[] source, int pos, String message) {
if( source==null || pos<0 || pos>source.length )
return name+": error: "+message+" (source unavailable at byte offset "+pos+")";
@@ -28,22 +32,24 @@ public abstract class SourceError {
int point=Math.min(pos,end);
String before=new String(source,start,point-start,StandardCharsets.UTF_8);
String text=new String(source,start,end-start,StandardCharsets.UTF_8);
- int column=before.codePointCount(0,before.length())+1;
- SB sb=new SB().p(name).p(':').p(line).p(':').p(column).p(": error: ").p(message).p("\n ");
+ SB excerpt=new SB();
int width=0, caret=0;
for( int i=0; i<text.length(); ) {
if( i==before.length() ) caret=width;
int ch=text.codePointAt(i);
if( ch=='\t' ) {
int spaces=8-(width%8);
- for( int j=0; j<spaces; j++ ) sb.p(' ');
+ for( int j=0; j<spaces; j++ ) excerpt.p(' ');
width+=spaces;
- } else { sb.p(new String(Character.toChars(ch))); width++; }
+ } else { excerpt.p(new String(Character.toChars(ch))); width++; }
i+=Character.charCount(ch);
}
if( before.length()==text.length() ) caret=width;
+ // Emacs compilation mode defaults to display columns, with 8-column tabs.
+ SB sb=new SB().p(name).p(':').p(line).p(':').p(caret+1).p(": error: ").p(message)
+ .p("\n ").p(excerpt.toString());
sb.p("\n ");
- for( int i=0; i<caret; i++ ) sb.p('-');
+ for( int i=0; i<caret; i++ ) sb.p('_');
return sb.p('^').toString();
}
}
diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java
index 91d27330..ac5e4eee 100644
--- a/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -242,7 +242,7 @@ public class CallNode extends CFGNode {
if( !tfp.notNull() )
return Parser.error( "Might be null calling "+tfp, _loc);
if( nargs() != tfp.nargs() )
- return Parser.error( "Expecting "+tfp.nargs()+" arguments, but found "+nargs(), _loc);
+ return Parser.error( "Expecting "+(tfp.nargs()-1)+" arguments, but found "+(nargs()-1), _loc);
// Check for args
for( int i=0; i<tfp.nargs(); i++ )
diff --git a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
index 09e5eac8..e49162f4 100644
--- a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
@@ -49,7 +49,7 @@ public class FldOffNode extends TypeNode {
@Override public Parser.ParseException err() {
if( in(1)._type instanceof TypeMemPtr tmp && tmp._obj._fref )
- return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null);
+ return Parser.error("Unknown struct type '"+tmp._obj._name+"'",_loc);
return null;
}
}
diff --git a/src/main/java/com/seaofnodes/simple/node/FormatNode.java b/src/main/java/com/seaofnodes/simple/node/FormatNode.java
new file mode 100644
index 00000000..d34b342d
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/FormatNode.java
@@ -0,0 +1,33 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.*;
+
+/** Resolve default formatting through normal type flow, including forward
+ * references. Valid programs fold this selector before serialization/codegen. */
+public final class FormatNode extends Node {
+ private final Lexer _site;
+ public FormatNode(Node value, Node ints, Node floats, Node strings, Lexer site) {
+ super(null,value,ints,floats,strings);
+ _site=site;
+ }
+ @Override public String label() { return "Format"; }
+ private int choice() {
+ Type t=in(1)._type;
+ if( t instanceof TypeInteger ) return 2;
+ if( t instanceof TypeFloat ) return 3;
+ if( t instanceof TypeMemPtr p && !p.nullable() && p._obj.isAry() &&
+ p._obj._name.equals("[]u8") ) return 4;
+ return 0;
+ }
+ @Override public Type compute() {
+ if( in(1)._type.isHigh() ) return Type.TOP;
+ int c=choice();
+ return c==0 ? in(2)._type.meet(in(3)._type).meet(in(4)._type) : in(c)._type;
+ }
+ @Override public Node idealize() { int c=choice(); return c==0 ? null : in(c); }
+ @Override public Parser.ParseException err() {
+ return Parser.error("Cannot format "+in(1)._type+"; expected an integer, float, or byte string",_site);
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index 85ab29f9..eb78ed1f 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -395,6 +395,9 @@ public class LoadNode extends MemOpNode {
// load will need to zero/sign-extend.
private Node extend(Node val) {
if( !(declaredType() instanceof TypeInteger ti) ) return val;
+ // A typed load can already know its width while the stored value is
+ // still unresolved. Do not replace it with an untyped shift/and chain.
+ if( !(val._type instanceof TypeInteger) ) { addDep(val); return null; }
if( ti._min==0 ) // Unsigned
return new AndNode(null,val,con(ti._max));
// Signed extension
diff --git a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
index 1130cf44..80d2aa0a 100644
--- a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
@@ -638,6 +638,7 @@ public class ScopeNode extends MemMergeNode {
int i = _guards._len;
while( i > 0 ) {
Node cast = _guards.at(--i);
+ if( cast instanceof FunNode ) break; // Function boundary
if( cast instanceof CFGNode ) continue; // Marker between guard sets
Node xpred = _guards.at(--i);
if( xpred == pred )
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
index 814a82c2..da9afe71 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/ExtendARM.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendARM extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendARM(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjARM(prj));
+ this(prj,new ProjARM(prj),ret);
+ }
+ ExtendARM(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmARM(parm),decl);
+ }
+ private ExtendARM(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
index 3c965da5..ee7b0fc3 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/arm/arm.java
@@ -332,7 +332,8 @@ public class arm extends Machine {
case NewNode nnn -> new NewARM(nnn);
case NotNode not -> new NotARM(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmARM(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendARM(parm,ti) : new ParmARM(parm);
case BulkMemPhiNode phi -> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
index 5bd3ec1c..11182133 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/ExtendRISC.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendRISC extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendRISC(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjRISC(prj));
+ this(prj,new ProjRISC(prj),ret);
+ }
+ ExtendRISC(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmRISC(parm),decl);
+ }
+ private ExtendRISC(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
index 0195542c..378955a5 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/riscv/riscv.java
@@ -236,7 +236,8 @@ public class riscv extends Machine {
case NewNode nnn -> nnn(nnn);
case NotNode not -> new NotRISC(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmRISC(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendRISC(parm,ti) : new ParmRISC(parm);
case BulkMemPhiNode phi-> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
index 1a6ae123..9f4da6b8 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/ExtendX86.java
@@ -6,14 +6,21 @@ import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.SB;
-// Convert a narrow C return register to Simple's full-width integer value.
+// Normalize narrow parameters and C return values to full-width integers.
public class ExtendX86 extends MachConcreteNode {
final int _bits;
final boolean _signed;
ExtendX86(ProjNode prj, TypeInteger ret) {
- super(prj);
- _inputs.set(0,null);
- _inputs.add(new ProjX86(prj));
+ this(prj,new ProjX86(prj),ret);
+ }
+ ExtendX86(ParmNode parm, TypeInteger decl) {
+ this(parm,new ParmX86(parm),decl);
+ }
+ private ExtendX86(Node ideal, Node value, TypeInteger ret) {
+ super(ideal);
+ _inputs.clear();
+ _inputs.add(null);
+ _inputs.add(value);
_bits = 8 << ret.log_size();
_signed = ret._min < 0;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
index 3dc93bf6..3d71b4ea 100644
--- a/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
+++ b/src/main/java/com/seaofnodes/simple/node/cpus/x86_64_v2/x86_64_v2.java
@@ -306,7 +306,8 @@ public class x86_64_v2 extends Machine {
case NewNode nnn -> new NewX86(nnn);
case NotNode not -> new NotX86(not);
case OrNode or -> or(or);
- case ParmNode parm -> new ParmX86(parm);
+ case ParmNode parm -> parm._declaredType instanceof TypeInteger ti && ti.log_size()<3
+ ? new ExtendX86(parm,ti) : new ParmX86(parm);
case BulkMemPhiNode phi-> new BulkMemPhiNode(phi);
case MemPhiNode phi -> new MemPhiNode(phi);
case PhiNode phi -> new PhiNode(phi);
@@ -555,6 +556,7 @@ public class x86_64_v2 extends Machine {
private static int off, scale, imm;
private static Node idx, val;
private <N extends MemOpNode > N address(N mop) {
+ externalAddress(mop);
off = scale = imm = 0; // Reset
idx = val = null;
Node base = mop.ptr();
diff --git a/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
index be68e10e..861f95b6 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Allocations.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
@@ -14,28 +14,30 @@ final class Allocations {
Parse an allocation
*/
Node parseNew() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
Type t = P.types.parseType();
if( t==null ) {
String tname = P._lexer.matchId();
if( tname==null || Parser.KEYWORDS.contains(tname) )
- throw P.error("Expected a type");
+ throw Parser.error("Expected a type after 'new'",site);
t = P.types.forwardRefType(tname);
}
// Parse ary[ length_expr ]
if( P.match("[") ) {
if( !t.makeZero().isa(t) )
- throw P.error("Cannot allocate a non-nullable, since arrays are always zero/null filled");
+ throw Parser.error("Cannot allocate a non-nullable, since arrays are always zero/null filled",site);
Node len = P.expressions.parseAssignment().keep();
P.require("]");
return allocArray(P.types.typeAry(t,false),len);
} else {
if( !(t instanceof TypeMemPtr tmp) )
- throw P.error("Cannot allocate a "+t.str());
- return allocStruct(tmp._obj);
+ throw Parser.error("Cannot allocate a "+t.str(),site);
+ return allocStruct(tmp._obj,site);
}
}
- private Node allocStruct(TypeStruct ts) {
+ private Node allocStruct(TypeStruct ts, Lexer site) {
Ary<Node> ctorArgs = constructorArgs();
// A field declaration can leave a short forward type name in TYPES.
// Allocation happens in lexical context, so resolve that short name
@@ -45,7 +47,7 @@ final class Allocations {
if( resolved instanceof TypeStruct rts ) ts = rts;
TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(typeName));
if( clz==null )
- throw P.error("Unknown struct type '" + ts._name + "'");
+ throw Parser.error("Unknown struct type '" + ts._name + "'",site);
Field afld = clz.field("<new>");
Node alloc;
if( afld != null ) {
@@ -56,7 +58,7 @@ final class Allocations {
: new FunPtrNode(tfp,P._code._start,fun.ret()).peephole();
} else {
if( !ts._fref )
- throw P.error("Constructor arguments for '" + ts._name + "' but no constructor is defined");
+ throw Parser.error("Constructor arguments for '" + ts._name + "' but no constructor is defined",site);
Type[] sig = new Type[ctorArgs._len];
for( int i = 0; i<ctorArgs._len; i++ )
sig[i] = ctorArgs.at(i)._type;
@@ -111,12 +113,12 @@ final class Allocations {
return self.unkeep();
}
- Node newString(String s) {
+ Node newString(byte[] s) {
TypeStruct ts = P.types.typeAry(TypeInteger.U8,true);
int lenAlias = ts.field("#" )._alias;
int elemAlias = ts.field("[]")._alias;
TypeConAryB body = TypeConAryB.make(s);
- TypeInteger slen = TypeInteger.constant(s.length());
+ TypeInteger slen = TypeInteger.constant(s.length);
// Make a TMP, not-null (byte)2, singleton (true) (requires text
// strings are hash-interned), with a TypeStruct having a constant
// array body.
diff --git a/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
index caa73076..6c626669 100644
--- a/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
+++ b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
@@ -14,6 +14,8 @@ final class ControlFlow {
// Parse a conditional expression, merging results.
Node conditional( Node pred, String fside ) {
+ Lexer site=P.loc();
+ if( !fside.equals("else") ) site._position-=fside.equals(":") ? 1 : 2;
// IfNode takes current control and predicate
pred.keep();
Node ifNode = new IfNode(P.ctrl(), pred).peephole();
@@ -88,7 +90,7 @@ final class ControlFlow {
Node ret = P.peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep()));
// Immediately fail e.g. `arg ? 7 : ptr`
ParseException err;
- if( !fside.equals("else") && (err = ret.err()) !=null ) throw err;
+ if( !fside.equals("else") && (err = ret.err()) !=null ) throw Parser.error(err.getMessage(),site);
ret.keep();
r.peephole();
return ret.unkeep();
diff --git a/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
index 91955197..13fa59b8 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Declarations.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
@@ -47,15 +47,17 @@ final class Declarations {
boolean inferType = t==Type.TOP || t==Type.BOTTOM;
boolean xfinal = bindingFinal(t);
Lexer loc = P.loc();
- String name = P.requireId();
+ String name = P.requireId(P._scope.klast() instanceof Kind.Func fun && FunNode.isInit(fun._name));
// Optional initializing expression follows
boolean fld_final = false; // Field is final, but not deeply final
boolean uninit = false; // Required non-null constructor field
Node expr;
if( P.match("=") ) {
+ P._lexer.skipWhiteSpace();
+ Lexer initializer=P.loc();
if( isExternDecl() ) {
if( !(t instanceof TypeFunPtr) ) {
- Node ptr = externData(name,t);
+ Node ptr = externData(name,t,loc);
// The lexical binding is a fixed address; its contents are mutable.
if( !P._scope.define(name,t,true,ptr,loc) )
throw Parser.error("Redefining name '"+name+"'",loc);
@@ -72,8 +74,12 @@ final class Declarations {
// var/val, then type comes from expression
if( inferType ) {
if( expr._type==Type.NIL )
- throw P.error("a not-null/non-zero expression");
+ throw Parser.error("Cannot infer a type from null; declare a nullable type explicitly",initializer);
t = expr._type;
+ // A typed expression may still await a forward callee's flow
+ // type (e.g. interpolation while sys.text is parsed). Its
+ // checked conversion already supplies a binding/storage type.
+ if( t==Type.BOTTOM && expr instanceof ConvertNode convert ) t=convert.dst();
if( !xfinal ) t = Conversions.mutableInferredType(t);
// Instance fields may change until the constructor finishes.
// Keep the declared family/access, not the default's constant
@@ -133,8 +139,7 @@ final class Declarations {
// Just after parsing "type foo = " can parse `"C"`
private boolean isExternDecl( ) {
int old = P.pos();
- String s = P._lexer.parseString();
- if( "C".equals(s) ) return true;
+ if( P.match("\"C\"") ) return true;
P.pos(old);
return false;
}
@@ -151,14 +156,14 @@ final class Declarations {
// A lexical C binding has a singleton namespace with one external field.
// The namespace occupies no storage; its field offset supplies the address.
- private Node externData(String name, Type t) {
+ private Node externData(String name, Type t, Lexer site) {
if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) )
- throw P.error("C data binding requires an integer or float type");
+ throw Parser.error("C data binding requires an integer or float type",site);
String cname = ("C:"+name).intern();
TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,P._code.externAlias(name),false,true));
Type old = Parser.TYPES.putIfAbsent(cname,ts);
if( old!=null && old!=ts )
- throw P.error("Conflicting types for C data '"+name+"'");
+ throw Parser.error("Conflicting types for C data '"+name+"'",site);
return P.con(TypeMemPtr.make((byte)2,ts,true));
}
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
index 93c07d8a..247d974e 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Expressions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -15,6 +15,8 @@ final class Expressions {
/** Parse: [name '='] expr
*/
Node parseAssignment() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
int old = P.pos();
String name = P._lexer.matchId();
// Just a plain expression, no assignment.
@@ -24,13 +26,16 @@ final class Expressions {
// Find variable to update
Var def = P._scope.lookup(name);
- if( def==null )
+ if( def==null ) {
+ P.pos(old);
+ P._lexer.skipWhiteSpace();
throw P.error("Undefined name '" + name + "'");
+ }
if( def._extern!=0 ) // Parse the C binding as a memory lvalue
{ P.pos(old); return parseExpression(); }
- P.references.checkAssignment(def);
+ P.references.checkAssignment(def,site);
return P.references.assign(def,parseAssignment());
}
@@ -117,6 +122,8 @@ final class Expressions {
Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool
// Stacked compares?
+ P._lexer.skipWhiteSpace();
+ Lexer operator=P.loc();
int dir0 = parseCompDir();
if( dir0 == 0 ) {
rhs.unkill();
@@ -137,7 +144,7 @@ final class Expressions {
// compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc.
while( dir0 != 0 ) {
if( Math.abs(dir) != Math.abs(dir0) )
- throw P.error("Mixing relational directions in a chained relational test");
+ throw Parser.error("Mixing relational directions in a chained relational test",operator);
// True side parses next arm of test
P.ctrl(ifT.unkeep());
lhs = rhs; // Reuse the previous RHS exactly once
@@ -150,6 +157,8 @@ final class Expressions {
P.ctrl(ifF);
fail.mergeScopes(P._scope.dup(), P.loc()).peephole();
// Check next stacked compare
+ P._lexer.skipWhiteSpace();
+ operator=P.loc();
dir0 = parseCompDir();
}
rhs.unkill();
@@ -185,9 +194,11 @@ final class Expressions {
private Node parseShift() {
Node lhs = parseAddition();
while( true ) {
- if( P.match("<<") ) lhs = new ShlNode(P.loc(),lhs,null);
- else if( P.match(">>>")) lhs = new ShrNode(P.loc(),lhs,null);
- else if( P.match(">>") ) lhs = new SarNode(P.loc(),lhs,null);
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
+ if( P.match("<<") ) lhs = new ShlNode(site,lhs,null);
+ else if( P.match(">>>")) lhs = new ShrNode(site,lhs,null);
+ else if( P.match(">>") ) lhs = new SarNode(site,lhs,null);
else break;
lhs.setDef(2,parseAddition());
ParseException err;
@@ -251,12 +262,14 @@ final class Expressions {
int old = P.pos();
if( P.match("--") || P.match("++") ) {
int delta = P._lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec
+ P._lexer.skipWhiteSpace();
int start = P.pos();
+ Lexer site=P.loc();
String name = P._lexer.matchId();
Var var = name==null ? null : P._scope.lookup(name);
if( var!=null && !(var.type() instanceof TypeMemPtr) ) {
if( var._extern==0 && var._final && !P._scope.canInit(var) )
- throw P.error("Cannot reassign final '"+var._name+"'");
+ throw Parser.error("Cannot reassign final '"+var._name+"'",site);
// Resolve fields just as a read or postfix update does. Updating
// their declaration's local slot would silently lose the Store.
P.pos(start);
@@ -286,7 +299,7 @@ final class Expressions {
// pre is +/-1 for a prefix update of a known name, zero otherwise.
private Node parsePrimary(int pre) {
if( P._lexer.isNumber(P._lexer.peek()) ) return parseLiteral();
- if( P._lexer.peek('"') ) return parsePostfix(P.allocations.newString(P._lexer.parseString()));
+ if( P._lexer.peek('"') ) return parsePostfix(P.strings.parse());
if( P.matchx("true" ) ) return P.con(1);
if( P.matchx("false") ) return P._code.ZERO;
if( P.matchx("null" ) ) return P.con(Type.NIL);
@@ -298,8 +311,10 @@ final class Expressions {
int pos = P.pos();
// Expect an identifier now
String id = P._lexer.matchId();
- if( id == null || Parser.KEYWORDS.contains(id) )
+ if( id == null || Parser.KEYWORDS.contains(id) ) {
+ P.pos(pos);
throw P.errorSyntax("an identifier or expression");
+ }
int pos2 = P.pos();
return P.references.parseName(id,pre,pos,pos2);
}
@@ -366,7 +381,11 @@ final class Expressions {
P._lexer.skipWhiteSpace();
Lexer site = P.loc();
String name;
- if( P.match(".") ) name = P.requireId();
+ if( P.match(".") ) {
+ P._lexer.skipWhiteSpace();
+ site=P.loc();
+ name = P.requireId(true);
+ }
else if( P.match("#") ) name = "#";
else if( P.match("[") ) name = "[]";
// Can get here without an explicit receiver: "f()" or
@@ -389,6 +408,6 @@ final class Expressions {
// Already parsed "'"
Node parseChar() {
- return P.require(P.con(TypeInteger.constant(P._lexer.parseChar())),"'");
+ return P.con(TypeInteger.constant(P._lexer.parseChar()));
}
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java
index 14dee33b..5175acc6 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Functions.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java
@@ -73,14 +73,19 @@ final class Functions {
while( true ) {
Type t = P.types.parseType(); // Arg type
if( t==null ) break;
+ P._lexer.skipWhiteSpace();
+ Lexer qualifier=P.loc();
if( P.match("^") ) {
if( !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() )
- throw P.error("'^' requires a struct parameter");
+ throw Parser.error("'^' requires a struct parameter",qualifier);
t = ptr.withPartial(true);
P.construction._borrowed = true;
}
finals.set(ids.size(),P.declarations.bindingFinal(t));
+ P._lexer.skipWhiteSpace();
+ Lexer nameLoc=P.loc();
String id = P.requireId();
+ if( ids.find(id)!=-1 ) throw Parser.error("Duplicate parameter '"+id+"'",nameLoc);
ts .push(t ); // Push type/arg pairs
ids.push(id);
P.match(",");
@@ -98,7 +103,7 @@ final class Functions {
Lexer site = P.loc();
site._position--; // Opening '('; arguments can contain nested calls.
if( fcn._type == Type.NIL )
- throw P.error("Calling a null function pointer");
+ throw Parser.error("Calling a null function pointer",site);
fcn.keep(); // Keep while parsing args
Ary<Node> args = new Ary<>( Node.class );
@@ -207,6 +212,9 @@ final class Functions {
int oldUID = P._code.UID(); // Used to approximate function size
FunNode fun = enterFunction(funName,sig,loc,finals,ids);
+ // Guards belong to this function's control flow. Do not inherit an
+ // enclosing function's predicates or leak this body's guards on exit.
+ P._scope.addGuards(fun,null,false);
Construction.Frame outerConstruction = P.construction.enter(fun,isCtor && !fun.isClz());
Node rpc = fun.parm(0);
if( fun.isClz() ) P.structs.beginClassInit(fun,loc);
@@ -260,6 +268,7 @@ final class Functions {
P._ref.addFun(P._code,fun);
// Unstack parser state
+ P._scope.removeGuards(fun);
if( _returnScope != null ) _returnScope.kill();
_returnScope = returnScope;
P.statements._continueScope = continueScope;
diff --git a/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
index ec88738b..5e4b7311 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Lexer.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -3,10 +3,12 @@ package com.seaofnodes.simple.parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
import com.seaofnodes.simple.codegen.Serialize;
+import com.seaofnodes.simple.codegen.SourceError;
import com.seaofnodes.simple.parser.Parser.ParseException;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
import java.util.HashMap;
+import java.io.ByteArrayOutputStream;
// Lexer provides low level access to the raw file bytes, peeks and matches
// short strings, parses numbers, skips comments and whitespace, tracks
@@ -35,6 +37,11 @@ public class Lexer {
public String position() { return _line_number+":"+(_position-_line_start+1); }
public int pos() { return _position; }
+ public String diagnostic(String message) {
+ return _cu==null ? SourceError.format("<input>",_input,_position,message)
+ : SourceError.format(_cu,_position,message);
+ }
+
/**
* Record the source text for lexing
*/
@@ -143,19 +150,21 @@ public class Lexer {
while( true ) {
if( isWhiteSpace() ) inc();
// Skip // to end of line
- else if( _position+2 < _input.length &&
+ else if( _position+1 < _input.length &&
_input[_position ] == '/' &&
_input[_position+1] == '/') {
inc(); inc();
while( !isEOF() && _input[_position] != '\n' )
inc();
- } else if( _position+2 < _input.length &&
+ } else if( _position+1 < _input.length &&
_input[_position ] == '/' &&
_input[_position+1] == '*') {
- // Skip /*comment*/
- while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/'))
+ Lexer start=new Lexer(this);
+ inc(); inc();
+ while( !isEOF() && !(peek()=='*' && _position+1<_input.length && peek(1)=='/') )
inc();
- inc();
+ if( isEOF() ) throw Parser.error("Unclosed block comment",start);
+ inc(); inc();
} else break;
}
}
@@ -229,20 +238,27 @@ public class Lexer {
// Return a constant Type, either TypeInteger or TypeFloat
Type parseNumber() {
+ Lexer start=new Lexer(this);
int old = _position;
int len = isLongOrDouble();
boolean f32 = peek()=='f';
if( f32 ) nextChar();
int n = Math.abs(len);
- if( f32 )
- return TypeFloat.constant(Float.parseFloat(new String(_input,old,n)));
- if( len > 0 ) {
- if( len > 1 && _input[old]=='0' )
- throw Parser.error("Syntax error: integer values cannot start with '0'",this);
- long i = Long.parseLong(new String(_input,old,len));
- return TypeInteger.constant(i);
+ String literal=new String(_input,old,n);
+ try {
+ if( f32 )
+ return TypeFloat.constant(Float.parseFloat(literal));
+ if( len > 0 ) {
+ if( len > 1 && _input[old]=='0' )
+ throw Parser.error("Syntax error: integer values cannot start with '0'",start);
+ long i = Long.parseLong(literal);
+ return TypeInteger.constant(i);
+ }
+ return TypeFloat.constant(Double.parseDouble(literal));
+ } catch( NumberFormatException ex ) {
+ throw Parser.error(len>0 && !f32 ? "Integer literal is outside the signed 64-bit range"
+ : "Invalid floating-point literal '"+literal+(f32 ? "f" : "")+"'",start);
}
- return TypeFloat.constant(Double.parseDouble(new String(_input,old,n)));
}
private String parseNumberString() {
int old = _position;
@@ -319,25 +335,77 @@ public class Lexer {
return 0;
}
- String parseString() {
- if( !peek('"') ) return null;
- inc();
- StringBuilder sb = new StringBuilder();
+ record StringPart(byte[] text, boolean end) {}
+
+ // Opening quote has been consumed. Leave a format's '%' unread so both
+ // the expression parser and deferred-body scanner use the same grammar.
+ StringPart stringPart(Lexer start) {
+ ByteArrayOutputStream sb = new ByteArrayOutputStream();
while( !isEOF() ) {
char c = nextChar();
- if( c=='"' ) return sb.toString();
+ if( c=='"' ) return new StringPart(sb.toByteArray(),true);
+ if( c=='%' ) {
+ if( peek()=='%' ) { inc(); sb.write('%'); continue; }
+ _position--;
+ return new StringPart(sb.toByteArray(),false);
+ }
if( c!='\\' ) {
- sb.append(c);
+ sb.write(c);
continue;
}
if( isEOF() )
- throw error("Unclosed string");
- sb.append(escapedChar("string"));
+ throw Parser.error("Unclosed string",start);
+ sb.write(escapedChar("string"));
}
- throw error("Unclosed string");
+ throw Parser.error("Unclosed string",start);
+ }
+
+ record Format(int kind, int flags, int width, int precision, Lexer site) {}
+
+ Format stringFormat() {
+ Lexer site = new Lexer(this);
+ assert peek()=='%';
+ inc();
+ if( peek()=='(' ) { inc(); return new Format(0,0,0,-1,site); }
+ int flags=0, bit;
+ while( (bit="-+ #0".indexOf(peek()))>=0 ) {
+ int mask=1<<bit;
+ if( (flags&mask)!=0 ) throw Parser.error("Repeated format flag",site);
+ flags |= mask;
+ inc();
+ }
+ int width = formatNumber(site);
+ int precision = -1;
+ if( peek()=='.' ) {
+ inc();
+ precision = formatNumber(site);
+ }
+ char kind=nextChar();
+ if( "diuoxXfFeEgGaAcsb".indexOf(kind)<0 )
+ throw Parser.error("Expected a string format: %(expression), %d(expression), or %%",site);
+ boolean number="diuoxXfFeEgGaA".indexOf(kind)>=0;
+ if( !number && (flags&~1)!=0 || "uoxX".indexOf(kind)>=0 && (flags&6)!=0 ||
+ "diu".indexOf(kind)>=0 && (flags&8)!=0 )
+ throw Parser.error("Invalid flags for %"+kind,site);
+ if( precision>=0 && (kind=='c' || kind=='b') )
+ throw Parser.error("Precision is not supported for %"+kind,site);
+ if( peek()!='(' ) throw Parser.error("Expected '(' after string format %"+kind,site);
+ inc();
+ return new Format(kind,flags,width,precision,site);
+ }
+
+ private int formatNumber(Lexer site) {
+ long n=0;
+ while( peek()>='0' && peek()<='9' ) {
+ n=n*10+nextChar()-'0';
+ if( n>Integer.MAX_VALUE ) throw Parser.error("String format width or precision is too large",site);
+ }
+ return (int)n;
}
private char escapedChar(String kind) {
+ Lexer start=new Lexer(this);
+ start._position--; // The backslash introducing the escape.
char esc = nextChar();
return switch( esc ) {
case '0' -> '\u0000';
@@ -347,17 +415,24 @@ public class Lexer {
case '\\' -> '\\';
case '"' -> '"';
case '\'' -> '\'';
- default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ case '%' -> '%';
+ default -> throw Parser.error("Unknown "+kind+" escape \\"+esc,start);
};
}
char parseChar() {
- if( isEOF() ) throw error("Unclosed character");
+ Lexer start=new Lexer(this);
+ start._position--; // Opening quote was consumed by the caller.
+ if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start);
+ if( peek()=='\'' ) throw Parser.error("Empty character literal",start);
char c = nextChar();
if( c=='\\' ) {
- if( isEOF() ) throw error("Unclosed character");
+ if( isEOF() ) throw Parser.error("Unclosed character literal",start);
c = escapedChar("character");
}
+ if( isEOF() || peek()=='\n' || peek()=='\r' ) throw Parser.error("Unclosed character literal",start);
+ if( peek()!='\'' ) throw Parser.error("Character literal must contain exactly one byte",start);
+ inc();
return c;
}
@@ -372,16 +447,18 @@ public class Lexer {
if( isEOF() ) throw error("Unclosed "+what);
char ch = peek();
if( ch==close && depth==0 ) return;
+ Lexer site=ch=='"' ? new Lexer(this) : null;
nextChar();
if( ch==open ) depth++;
else if( ch==close ) depth--;
- else if( ch=='"' || ch=='\'' ) {
- char quote = ch;
- while( !isEOF() ) {
- ch = nextChar();
- if( ch=='\\' ) nextChar();
- else if( ch==quote ) break;
+ else if( ch=='"' ) {
+ while( !stringPart(site).end() ) {
+ stringFormat();
+ skipTo('(',')',"string interpolation");
+ nextChar();
}
+ } else if( ch=='\'' ) {
+ parseChar();
}
}
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java
index 4d140e55..0cf3c0e7 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Parser.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java
@@ -24,6 +24,7 @@ public class Parser {
final References references = new References(this);
final Conversions conversions = new Conversions(this);
final Construction construction = new Construction(this);
+ final Strings strings = new Strings(this);
public void checkConstructionSerializable() { construction.checkSerializable(); }
@@ -106,7 +107,7 @@ public class Parser {
put("i32" ,TypeInteger.I32);
put("i64" ,TypeInteger.BOT);
put("i8" ,TypeInteger.I8 );
- put("int" ,TypeInteger.BOT);
+ put("int" ,TypeInteger.I32);
put("u1" ,TypeInteger.U1 );
put("u16" ,TypeInteger.U16);
put("u32" ,TypeInteger.U32);
@@ -117,7 +118,8 @@ public class Parser {
public static class ParseException extends RuntimeException {
public final Lexer _loc;
- ParseException( String msg, Lexer loc ) { super(msg); _loc = loc; }
+ ParseException( String msg, Lexer loc ) { super(msg); _loc = loc==null ? null : new Lexer(loc); }
+ public String diagnostic() { return _loc==null ? getMessage() : _loc.diagnostic(getMessage()); }
}
public Parser(CodeGen code ) {
@@ -186,9 +188,18 @@ public class Parser {
// Require and return an identifier
String requireId() {
+ return requireId(false);
+ }
+
+ // Primitive names are useful member names (sys.text.int), but remain
+ // reserved for local variables, parameters and type declarations.
+ String requireId(boolean member) {
+ _lexer.skipWhiteSpace();
+ Lexer site=loc();
String id = _lexer.matchId();
- if (id != null && !KEYWORDS.contains(id) ) return id.intern();
- throw error("Expected an identifier, found '"+id+"'");
+ if (id != null && (!KEYWORDS.contains(id) || member &&
+ (INIT_TYPES.get(id) instanceof TypeInteger || INIT_TYPES.get(id) instanceof TypeFloat)) ) return id.intern();
+ throw error("Expected an identifier, found '"+new Lexer(site).getAnyNextToken()+"'",site);
}
String matchId() {
@@ -266,7 +277,9 @@ public class Parser {
ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); }
private ParseException _errorSyntax(String msg) {
- return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`");
+ _lexer.skipWhiteSpace();
+ // Reading the token for the message must not advance the error location.
+ return error("Syntax error, "+msg+" but found `" + loc().getAnyNextToken()+"`");
}
ParseException error(String msg) { return error(msg,_lexer); }
public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); }
diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md
index ac0d9f29..cee1d64a 100644
--- a/src/main/java/com/seaofnodes/simple/parser/README.md
+++ b/src/main/java/com/seaofnodes/simple/parser/README.md
@@ -2,7 +2,7 @@
The parser builds Sea-of-Nodes IR directly. Each component shares the cursor,
active scope, and compilation state through its `Parser P` reference; there is no intermediate
-AST. The [grammar map](../../../../../../../docs/chapter29/29-grammar.md) connects language
+AST. The [grammar map](../../../../../../../docs/chapter30/30-grammar.md) connects language
productions to their entry points.
| File | Responsibility |
diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java
index 483bd7a8..6335bad2 100644
--- a/src/main/java/com/seaofnodes/simple/parser/References.java
+++ b/src/main/java/com/seaofnodes/simple/parser/References.java
@@ -15,13 +15,36 @@ final class References {
References(Parser P) { this.P = P; }
+ // Compiler-generated library references use ordinary module lookup and
+ // forward resolution, without a synthetic token stream.
+ Node classReference(String name, Lexer site) {
+ CompUnit cu=ParseAll.findCompUnit(P._code,P._ref,name);
+ if( cu==null ) throw Parser.error("Cannot find standard library '"+name+"'",site);
+ String clzName=TypeStruct.addClzPrefix(cu._cname);
+ TypeStruct clz=(TypeStruct)Parser.TYPES.computeIfAbsent(clzName,n -> TypeStruct.make(n,true));
+ FRefNode ref=new FRefNode(name,site,TypeMemPtr.make((byte)2,clz,true));
+ _frefs.push(ref);
+ return ref.peephole();
+ }
+
+ // Read a member without consuming a postfix from source text.
+ Node member(Node receiver, String name, Lexer site) {
+ Field fld=receiver._type instanceof TypeMemPtr ptr ? ptr._obj.field(name) : null;
+ return P.peep(new LoadNode(site,name,fld==null ? 1 : fld._alias,
+ P.ctrl(),P.mem(),receiver,P.fldoff(receiver,name)));
+ }
+
// Resolve a primary name, retaining its use site before speculative type lookup.
Node parseName(String id, int pre, int pos, int pos2) {
+ Lexer site=P.loc();
+ site._position=pos;
+ site.skipWhiteSpace();
// Attempt a local var lookup first
Var var = P._scope.lookup(id);
if( var==null ) {
// Not a local var, try again as a type name
P.pos(pos);
+ Lexer nameLoc = P.loc();
Type t = P.types.parseType();
if( t!=null ) {
// Static <clinit> typename lookup
@@ -52,7 +75,7 @@ final class References {
t = TypeMemPtr.make((byte)2,clz,true);
}
// Define the FRef
- var = P._scope.defineFRef(id,t,true,P.loc());
+ var = P._scope.defineFRef(id,t,true,nameLoc);
P.references._frefs.push((FRefNode)P._scope.in(var));
}
@@ -73,7 +96,7 @@ final class References {
if( kx < fx && !P.structs.inExplicitConstructor() && // Out of scope
!(var._final && rvalue._type.isConstant()) ) { // Not a final constant
if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke)
- throw P.error("Variable '"+var._name+"' is out of function scope and must be a final constant");
+ throw Parser.error("Variable '"+var._name+"' is out of function scope and must be a final constant",site);
// var.idx==2 is self argument, 3 is args, 4 or more are now static globals
if( var._idx >= 4 ) {
@@ -98,7 +121,7 @@ final class References {
}
// Assign-update direct into Scope
if( var._final && !P._scope.canInit(var) )
- throw P.error("Cannot reassign final '"+var._name+"'");
+ throw Parser.error("Cannot reassign final '"+var._name+"'",site);
checkSharedFunction(var);
Node op = P.expressions.opAssign(ch,rvalue, var.type() );
P._scope.update(var,op);
@@ -106,14 +129,14 @@ final class References {
return pre==0 && Lexer.postfix(ch) ? rvalue : op;
}
- void checkAssignment(Var def) {
+ void checkAssignment(Var def, Lexer site) {
String name = def._name;
// A final field may receive its first value in its constructor.
if( P._scope.in(def._idx)._type!=Type.TOP && def._final &&
// Inside an allocation, final assign is OK, outside nope.
// The parseNew() call added the allocation scope
!P._scope.canInit(def) )
- throw P.error("Cannot reassign final '"+name+"'");
+ throw Parser.error("Cannot reassign final '"+name+"'",site);
checkSharedFunction(def);
}
@@ -201,7 +224,7 @@ final class References {
if( fld==null && ts!=null )
fld = P.types.fieldOrOpen(ts,name,false);
if( fld==null && ts!=null && !P.types.latestStruct(ts)._open )
- throw P.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'");
+ throw Parser.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'",site);
// We have to record "active touches" of a new class, to track when we
// need to force class initialization. Basically, we insert an CLInit,
@@ -233,7 +256,7 @@ final class References {
// Store to field
mem.keep();
if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
- throw P.error("Cannot modify final field '"+name+"'");
+ throw Parser.error("Cannot modify final field '"+name+"'",site);
Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), lift, P.construction.own(expr)).peephole().keep();
P.construction.store(expr,alias,st,mem);
st.unkeep();
@@ -250,17 +273,17 @@ final class References {
decl = conary.elem();
// Load field
- Node load = P.peep(new LoadNode(P.loc(),name,alias,P.ctrl(),mem,expr,off));
+ Node load = P.peep(new LoadNode(site,name,alias,P.ctrl(),mem,expr,off));
// Check for assign-update, "ptr.fld += expr" or "ary[idx]++"
char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre;
if( ch!=0 ) {
if( fld!=null && decl==Type.BOTTOM )
- throw P.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" );
+ throw Parser.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor",site );
Node op = P.expressions.opAssign(ch,load, decl ).keep();
mem.keep();
if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
- throw P.error("Cannot modify final field '"+name+"'");
+ throw Parser.error("Cannot modify final field '"+name+"'",site);
Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), op, P.construction.own(expr)).peephole().keep();
P.construction.store(expr,alias,st,mem);
st.unkeep();
@@ -302,7 +325,8 @@ final class References {
Ary<FRefNode> frefs = new Ary<>(FRefNode.class);
for( FRefNode fref : _frefs ) {
if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue;
- CompUnit cu = ParseAll.findCompUnitOrThrow(P._code,P._ref,fref._name);
+ CompUnit cu = ParseAll.findCompUnit(P._code,P._ref,fref._name);
+ if( cu==null ) throw fref.err();
// Name lookup may already know this class's fields. Do not replace
// that information with an empty placeholder before Iter runs.
if( fref._con==FRefNode.FREF_TYPE )
diff --git a/src/main/java/com/seaofnodes/simple/parser/Statements.java b/src/main/java/com/seaofnodes/simple/parser/Statements.java
index 4ff4993b..707202ab 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Statements.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Statements.java
@@ -24,12 +24,14 @@ final class Statements {
* @return a {@link Node} or {@code null}
*/
Node parseStatement() {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
if( P.matchx("return") ) return parseReturn();
else if (P.matchx("if") ) return parseIf();
else if (P.matchx("while") ) return parseWhile();
else if (P.matchx("for") ) return parseFor();
- else if (P.matchx("break") ) return parseBreak();
- else if (P.matchx("continue")) return parseContinue();
+ else if (P.matchx("break") ) return parseBreak(site);
+ else if (P.matchx("continue")) return parseContinue(site);
else if (P.matchx("struct") ) return P.structs.parseStruct();
else if (P.structs.inStructDeclaration() && P.matchx("new")) return P.structs.parseConstructorDeclaration();
else if (P.matchx(";") ) return P._code.ZERO; // Empty statement
@@ -204,7 +206,7 @@ final class Statements {
if( doFor ) {
int old = P.pos(nextPos);
if( !P.peek(')') )
- P.expressions.parseAssignment();
+ P.expressions.parseAssignment().isKill();
if( P.pos() != nextEnd )
throw P.errorSyntax( "Unexpected code after expression" );
P.pos(old);
@@ -237,8 +239,8 @@ final class Statements {
P._scope = exit;
return P._code.ZERO;
}
- private Node parseBreak () {
- checkLoopActive();
+ private Node parseBreak(Lexer site) {
+ checkLoopActive(site);
// At the time of the break, and loop-exit conditions are only valid if
// they are ALSO valid at the break. It is the intersection of
// conditions here, not the union.
@@ -248,15 +250,20 @@ final class Statements {
return P._code.ZERO;
}
- private Node parseContinue() { checkLoopActive(); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; }
+ private Node parseContinue(Lexer site) { checkLoopActive(site); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; }
private ScopeNode jumpTo(ScopeNode toScope) {
ScopeNode cur = P._scope.dup();
P.ctrl(P._code.XCTRL); // Kill current scope
// Prune nested lexical scopes that have depth > than the loop head
// We use _breakScope as a proxy for the loop head scope to obtain the depth
+ // The clone shares lexical boundaries and Var records with the active
+ // parser scope. Promoting on the clone would move those boundaries
+ // without moving the active scope's bindings. Drop locals first, then
+ // copy forward references into the jump scope with their own indices.
while( cur.depth() > _breakScope.depth() )
- cur.pop();
+ cur._pop();
+ cur.balanceLoopFRefs(P._scope);
// If this is a continue then first time the target is null
// So we just use the pruned current scope as the base for the
// "continue"
@@ -264,9 +271,13 @@ final class Statements {
return cur;
// toScope is either the break scope, or a scope that was created here
assert toScope.depth() <= _breakScope.depth();
+ // A later path can discover forward references after an earlier
+ // break/continue saved its scope. Keep those bindings across the merge.
+ toScope.balanceLoopFRefs(cur);
+ cur.balanceLoopFRefs(toScope);
toScope.ctrl(toScope.mergeScopes(cur,P.loc()).peephole());
return toScope;
}
- private void checkLoopActive() { if (_breakScope == null) throw P.error("No active loop for a break or continue"); }
+ private void checkLoopActive(Lexer site) { if (_breakScope == null) throw Parser.error("No active loop for a break or continue",site); }
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Strings.java b/src/main/java/com/seaofnodes/simple/parser/Strings.java
new file mode 100644
index 00000000..5bf76585
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Strings.java
@@ -0,0 +1,59 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Interpolation expands to ordinary sys.text calls. The library itself uses
+ * no interpolation, so it can be compiled from source without staging. */
+final class Strings {
+ private final Parser P;
+ Strings(Parser p) { P=p; }
+
+ Node parse() {
+ Lexer start=P.loc();
+ P.require("\"");
+ Lexer.StringPart part=P._lexer.stringPart(start);
+ if( part.end() ) return P.allocations.newString(part.text());
+ Node text=P.references.classReference("sys.text",P.loc()).keep();
+ P.classInit(P.loc(),text);
+ Node builder=call(text,"begin",P.loc()).keep();
+ while( true ) {
+ if( part.text().length!=0 )
+ call(text,"string",P.loc(),builder,P.allocations.newString(part.text()),P.con('s'),P.con(0),P.con(-1)).isKill();
+ if( part.end() ) break;
+ Lexer.Format fmt=P._lexer.stringFormat();
+ Node value=P.expressions.parseAssignment().keep();
+ P.require(")");
+ Node fptr;
+ if( fmt.kind()==0 ) {
+ Node ints=P.references.member(text,"integer",fmt.site()).keep();
+ Node floats=P.references.member(text,"floating",fmt.site()).keep();
+ Node strings=P.references.member(text,"string",fmt.site()).keep();
+ fptr=new FormatNode(value,ints.unkeep(),floats.unkeep(),strings.unkeep(),fmt.site()).peephole();
+ } else {
+ String family="diuoxXcb".indexOf(fmt.kind())>=0 ? "integer" : fmt.kind()=='s' ? "string" : "floating";
+ fptr=P.references.member(text,family,fmt.site());
+ }
+ call(text,fptr,fmt.site(),builder,value.unkeep(),P.con(fmt.kind() | fmt.flags()<<8),
+ P.con(fmt.width()),P.con(fmt.precision())).isKill();
+ part=P._lexer.stringPart(start);
+ }
+ // String syntax promises this result type even when sys.text is still
+ // awaiting its source parse. Check/convert through the normal IR.
+ Node result=call(text.unkeep(),"end",P.loc(),builder.unkeep());
+ return P.conversions.convertExpr(result,TypeMemPtr.make(P.types.typeAry(TypeInteger.U8,true)).makeRO());
+ }
+
+ private Node call(Node text, String name, Lexer site, Node... values) {
+ return call(text,P.references.member(text,name,site),site,values);
+ }
+
+ private Node call(Node text, Node fptr, Lexer site, Node... values) {
+ Ary<Node> args=new Ary<>(Node.class);
+ args.push(P.ctrl()); args.push(P.mem()); args.push(text);
+ for( Node value:values ) args.push(value);
+ args.push(fptr);
+ return P.functions.functionCall(args,site);
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Structs.java b/src/main/java/com/seaofnodes/simple/parser/Structs.java
index a5262440..3a3d894c 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Structs.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Structs.java
@@ -78,12 +78,14 @@ final class Structs {
Node parseConstructorDeclaration() {
TypeStruct self = P._scope.constructorSelf();
assert self != null;
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
String name = P.requireId();
String expect = initFieldName(self._name);
if( name != expect )
- throw P.error("Constructor name '" + name + "' must match type name '" + expect + "'");
+ throw Parser.error("Constructor name '" + name + "' must match type name '" + expect + "'",site);
P.require("=");
- if( _pendingCtor!=null ) throw P.error("Redefining constructor '"+name+"'");
+ if( _pendingCtor!=null ) throw Parser.error("Redefining constructor '"+name+"'",site);
_pendingCtor = new Constructor(name,P.loc());
// Parse the body after all field declarations are known, so self.x
// and x resolve the same field even when x is declared below this body.
diff --git a/src/main/java/com/seaofnodes/simple/parser/Types.java b/src/main/java/com/seaofnodes/simple/parser/Types.java
index ffd3a0f4..827518fd 100644
--- a/src/main/java/com/seaofnodes/simple/parser/Types.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Types.java
@@ -54,12 +54,14 @@ final class Types {
// Nest arrays and '?' as needed
Type t2 = t1;
while( true ) {
+ P._lexer.skipWhiteSpace();
+ Lexer site=P.loc();
if( P.match("?") ) {
if( t2 instanceof TypeMemPtr tmp ) {
- if( tmp.nullable() ) throw P.error("Type "+t2+" already allows null");
+ if( tmp.nullable() ) throw Parser.error("Type "+t2+" already allows null",site);
t2 = tmp.makeNullable();
} else
- throw P.error("Type "+t0+" cannot be null");
+ throw Parser.error("Type "+t0+" cannot be null",site);
} else if( P.match("[~]") ) {
t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO();
} else if( P.match("[]") ) {
diff --git a/src/main/java/com/seaofnodes/simple/parser/package-info.java b/src/main/java/com/seaofnodes/simple/parser/package-info.java
index 7212e629..c886f72a 100644
--- a/src/main/java/com/seaofnodes/simple/parser/package-info.java
+++ b/src/main/java/com/seaofnodes/simple/parser/package-info.java
@@ -7,7 +7,7 @@
* use-side name/field resolution; Allocations, ControlFlow, and Conversions
* contain their shared IR construction. Lexer also supplies location snapshots.
*
- * <p>See {@code chapter29/docs/29-grammar.md} for the production-to-method map.
+ * <p>See {@code chapter30/docs/30-grammar.md} for the production-to-method map.
* Evaluation order and scope lifetimes are explicit: loop updates and declared
* constructor bodies are the two deferred source fragments. There is no AST.
*/
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
index c9093e3b..9cb23e09 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeConAry.java
@@ -16,6 +16,7 @@ public abstract class TypeConAry<A> extends TypeScalar {
TypeConAry( boolean any, byte widen, A ary ) { super(TCONARY); _any = any; _widen = widen; _ary = ary; }
public static void gather(ArrayList<Type> ts) {
+ ts.add(TypeConAryB.make(""));
ts.add(TypeConAryB.ABC);
ts.add(TypeConAryB.ABCD);
ts.add(TypeConAryI.I123);
@@ -36,6 +37,15 @@ public abstract class TypeConAry<A> extends TypeScalar {
}
Type ymeet( TypeInteger ti ) {
+ // Empty arrays have no observed elements: use the storage bounds.
+ // Check both endpoints just as the loop checks a nonempty body;
+ // retaining high integer inputs here preserves lattice duality.
+ if( len()==0 ) {
+ TypeInteger bound=emptyElem();
+ if( !ti.isa(TypeInteger.make(bound._min,bound._min,ti._widen)) ||
+ !ti.isa(TypeInteger.make(bound._max,bound._max,ti._widen)) )
+ return ti.meet(bound);
+ }
// if i can isa *each* element, then maybe can keep.
// no good to i.isa(elem()) because fails the dual
for( int j=0; j<len(); j++ )
@@ -64,6 +74,7 @@ public abstract class TypeConAry<A> extends TypeScalar {
return _any ? Type.TOP : BOTTOM;
int len = len();
long min = Long.MAX_VALUE;
+ if( len==0 ) return emptyElem();
long max = Long.MIN_VALUE;
for( int i=0; i<len; i++ ) {
min = Math.min(min,at8(i));
@@ -71,6 +82,10 @@ public abstract class TypeConAry<A> extends TypeScalar {
}
return TypeInteger.make(min,max,_widen);
}
+ private TypeInteger emptyElem() {
+ return log_size()==0 ? TypeInteger.make(0,255,_widen)
+ : TypeInteger.make(Integer.MIN_VALUE,Integer.MAX_VALUE,_widen);
+ }
public abstract long at8(int idx);
public abstract int len();
@Override public abstract int log_size();
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
index 5877c309..84be5fb2 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeConAryB.java
@@ -10,7 +10,7 @@ public class TypeConAryB extends TypeConAry<byte[]> {
public static TypeConAryB make( String s ) { return make(s.getBytes()); }
static final TypeConAryB ABC = make("abc");
static final TypeConAryB ABCD = make("abcd");
- @Override public long at8(int idx) { return _ary[idx]; }
+ @Override public long at8(int idx) { return _ary[idx]&255; }
@Override public int len() { return _ary.length; }
@Override public int log_size() { return 0; }
@Override public String str() { return "[\""+new String(_ary)+"\"]"; }
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
index a0c2266d..6164f721 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeInteger.java
@@ -53,7 +53,15 @@ public class TypeInteger extends TypeScalar {
}
public Type same_but_slightly_wider_than( ) {
- return _widen < 3 ? make(_min,_max, (byte)(_widen+1)) : BOT;
+ if( _widen < 3 ) return make(_min,_max, (byte)(_widen+1));
+ // Start in the intersection of i32 and u32 for nonnegative ranges.
+ // Widening those straight to i32 invents negative values that prevent
+ // a u32 loop from remaining unsigned. Either 32-bit range can then
+ // grow to its own bound; unbounded arithmetic ultimately reaches BOT.
+ if( _min >= 0 && _max <= I32._max ) return make(0,I32._max,(byte)3);
+ if( _min >= I32._min && _max <= I32._max ) return I32;
+ if( _min >= 0 && _max <= U32._max ) return U32;
+ return BOT;
}
// Force max widen
@Override public Type widen() {
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp
index 7705a187..1201315b 100644
--- a/src/main/smp/sys.smp
+++ b/src/main/smp/sys.smp
@@ -7,5 +7,7 @@ val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
val io = io; // IO library
+val text = text; // Typed text conversions
val libc = libc; // libc FFI bindings
+val rt = rt; // Run Time support
val scan = scan; // A simple string parser
diff --git a/src/main/smp/sys/ary.smp b/src/main/smp/sys/ary.smp
index 8312e94c..c139cd33 100644
--- a/src/main/smp/sys/ary.smp
+++ b/src/main/smp/sys/ary.smp
@@ -1,11 +1,25 @@
// Array utilities
+// Geometric growth without wrapping a u32 length. Zero means too large.
+val capacity = { u32 current, i64 needed ->
+ if( needed < 0 || needed > 4294967295 ) return 0;
+ if( needed <= current ) return current;
+ i64 size = current ? current : 1;
+ while( size < needed ) {
+ size *= 2;
+ if( size > 4294967295 ) size = 4294967295;
+ }
+ u32 result = size;
+ return result;
+};
+
// Copy byte array contents. Copies the shorter of the buffers and len
-val copy = { u8[] dst, int doff, u8[~] src, int soff, int len ->
+val copy = { u8[] dst, u32 doff, u8[~] src, u32 soff, u32 len ->
+ if( doff > dst# || soff > src# ) return dst;
if( len > dst# - doff ) len = dst# - doff;
if( len > src# - soff ) len = src# - soff;
- for( int i=0; i<len; i++ )
+ for( u32 i=0; i<len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
@@ -14,10 +28,12 @@ val copy = { u8[] dst, int doff, u8[~] src, int soff, int len ->
// len. Returns <0 or >0 for first non-matching byte. Returns <0 or >0 for
// the larger array after that, or 0 if both arrays are fully equal.
val cmp = { u8[~] src1, u32 off1, u8[~] src2, u32 off2, u32 len ->
- u32 len1 = len > src1# - off1 ? src1# - off1 : len;
- u32 len2 = len > src2# - off2 ? src2# - off2 : len;
+ u32 left1 = off1 >= src1# ? 0 : src1# - off1;
+ u32 left2 = off2 >= src2# ? 0 : src2# - off2;
+ u32 len1 = len > left1 ? left1 : len;
+ u32 len2 = len > left2 ? left2 : len;
u32 max = len1 > len2 ? len2 : len1;
- for( int i=0; i<max; i++ )
+ for( u32 i=0; i<max; i++ )
if( src1[off1+i] != src2[off2+i] )
return src1[off1+i] - src2[off2+i]; // Bytes determine compare
return len1 - len2; // Length determines compare
diff --git a/src/main/smp/sys/aryi64.smp b/src/main/smp/sys/aryi64.smp
index 98b72605..b9f1fbed 100644
--- a/src/main/smp/sys/aryi64.smp
+++ b/src/main/smp/sys/aryi64.smp
@@ -9,36 +9,36 @@ struct aryi64 {
};
// Grow as needed
- val _grow = { int len2 ->
- if( len+len2 < buf# ) return self;
- int newlen = buf# * 2;
- while( len+len2 >= newlen ) newlen *= 2;
+ val _grow = { u32 len2 ->
+ if( len+len2 <= buf# ) return self;
+ u32 newlen = ary.capacity(buf#,len+len2);
+ if( !newlen ) libc._exit(1); // Length exceeds the array representation.
i64[] !buf2 = new i64[newlen];
- for( int i=0; i<buf#; i++ )
+ for( u32 i=0; i<len; i++ )
buf2[i] = buf[i];
buf = buf2;
self;
};
- // Add a i64 to aryu8
+ // Add an i64 to aryi64
val add = { i64 c ->
_grow(1).buf[len++] = c;
self;
};
- val get = { int index -> return buf[index]; };
+ val get = { u32 index -> return buf[index]; };
// Write the bracketed array-list form to aryu8.
val str = { ->
// TODO: clunky default constructor syntax
var str = new aryu8().add('[');
- for( int i=0; i<len; i++ )
+ for( u32 i=0; i<len; i++ )
str.num(buf[i]).add(',').add(' ');
if( len ) str.undo(2);
return str.add(']');
};
- val swap = { int i, int j ->
+ val swap = { u32 i, u32 j ->
i64 tmp = buf[i];
buf[i] = buf[j];
buf[j] = tmp;
@@ -75,8 +75,11 @@ val parseAryI64 = { u8[~] str ->
return s.x == s.buf# ? nums : null;
};
-val copy = { i64[] dst, int doff, i64[~] src, int soff, int len ->
- for( int i=0; i<len; i++ )
+val copy = { i64[] dst, u32 doff, i64[~] src, u32 soff, u32 len ->
+ if( doff > dst# || soff > src# ) return dst;
+ if( len > dst# - doff ) len = dst# - doff;
+ if( len > src# - soff ) len = src# - soff;
+ for( u32 i=0; i<len; i++ )
dst[i+doff] = src[i+soff];
return dst;
};
diff --git a/src/main/smp/sys/aryu8.smp b/src/main/smp/sys/aryu8.smp
index 21cf920b..18543f13 100644
--- a/src/main/smp/sys/aryu8.smp
+++ b/src/main/smp/sys/aryu8.smp
@@ -7,12 +7,12 @@ struct aryu8 {
new aryu8 = { -> buf = new u8[1]; };
// Grow as needed
- val _grow = { int len2 ->
- if( len+len2 < buf# ) return self;
- int newlen = buf# * 2;
- while( len+len2 >= newlen ) newlen *= 2;
+ val _grow = { u32 len2 ->
+ if( len+len2 <= buf# ) return self;
+ u32 newlen = ary.capacity(buf#,len+len2);
+ if( !newlen ) libc._exit(1); // Length exceeds the array representation.
u8[] !buf2 = new u8[newlen];
- ary.copy(buf2,0,buf,0,buf#);
+ ary.copy(buf2,0,buf,0,len);
buf = buf2;
self;
};
@@ -27,38 +27,28 @@ struct aryu8 {
// Add all of a plain array to the aryu8
val addAry = { u8[~] xs ->
- _grow(xs#);
- ary.copy(buf,len,xs,0,xs#);
- len += xs#;
+ u32 n=xs#;
+ _grow(n);
+ ary.copy(buf,len,xs,0,n);
+ len += n;
self;
};
// Remove x bytes from the tail.
- val undo = { int x ->
+ val undo = { u32 x ->
len = len >= x ? len-x : 0;
self;
};
// Print ASCII number into aryu8
val num = { i64 x ->
- if( !x ) { add('0'); return self; }
- if( x < 0 ) { add('-'); x = -x; }
- i64 ten = 1, log = 0;
- while( ten <= x ) { ten *= 10; log++; }
- for( int i=0; i<log; i++ ) {
- ten /= 10;
- i64 q = x/ten;
- u8 digit = q+'0';
- add(digit);
- x -= q*ten;
- }
+ text.integer(self,x,0,0,-1);
self;
};
- // Write to fd, returns the number of bytes written (len).
+ // Write all bytes to fd; return len, or -1 with errno set on failure.
val write = { i32 fd ->
- i64 ptr = buf; // Cast buffer pointer to i64 for native C call
- libc.write(fd,ptr,len);
+ io.writeOff(fd,buf,0,len);
};
};
@@ -66,7 +56,7 @@ struct aryu8 {
// Parse one integer, ignore trailing data; returns 0 with an empty string.
// If you want a more robust parsing, use the `scan` class
val atoi = { u8[~] str ->
- int x = 0;
+ u32 x = 0;
while( x < str# && str[x] <= ' ' ) x++;
if( x == str# ) return 0;
bool neg = false;
@@ -77,11 +67,12 @@ val atoi = { u8[~] str ->
return neg ? -n : n;
};
-// Zero-terminate string for libc strings. No error checking, just copies and
-// appends a zero.
+// Zero-terminate a string for libc, reserving one array element for the zero.
{ u8[~] -> u8[] } str0 = { u8[~] str ->
+ if( str# == 4294967295 ) libc._exit(1);
u8[] !strz = new u8[str#+1];
- ary.copy(strz,0,str,0,str#);
+ u32 len=str#;
+ ary.copy(strz,0,str,0,len);
strz[str#] = '\0';
strz;
};
diff --git a/src/main/smp/sys/io.smp b/src/main/smp/sys/io.smp
index d6263ffe..14993429 100644
--- a/src/main/smp/sys/io.smp
+++ b/src/main/smp/sys/io.smp
@@ -4,43 +4,74 @@
// Write byte array to stdout
val p = { u8[~] str -> write(1,str); };
val p_noInline = { u8[~] str -> write(1,str); };
+// Write to stderr
+val err = { u8[~] str -> write(libc.STDERR,str); };
-// Return fd contents as a u8[].
+
+// Read until EOF, including after short reads. Return an empty array for an
+// empty stream, or null on error with errno preserved (partial data discarded).
val read = { i32 fd ->
- var buf = new u8[8*1024];
- i64 ptr = buf;
- int len = 0;
+ val chunk = new u8[8*1024];
+ i64 ptr = chunk;
+ val buf = new aryu8();
while( true ) {
- i32 rez = libc.read(fd,ptr,buf#);
- if( rez > buf# ) libc._exit(libc.errno());
- len += rez;
- if( rez < buf# )
- break;
- libc._exit(-2); // Need to implement for more than 8K input
+ i64 rez = libc.read(fd,ptr,chunk#);
+ if( rez < 0 ) {
+ if( libc.errno() == libc.EINTR ) continue;
+ return null;
+ }
+ if( rez == 0 ) break;
+ if( buf.len+rez > 4294967295 ) {
+ libc.set_errno(libc.EOVERFLOW);
+ return null;
+ }
+ u32 count = rez;
+ buf._grow(count);
+ ary.copy(buf.buf,buf.len,chunk,0,count);
+ buf.len += count;
}
- var out = new u8[len];
- ary.copy(out,0,buf,0,len);
+ val out = new u8[buf.len];
+ return ary.copy(out,0,buf.buf,0,buf.len);
};
-// Write all of str to fd.
+// Write all bytes, returning the count or -1 with errno set. An error can
+// follow a successfully written prefix; it does not undo that prefix.
val write = { i32 fd, u8[~] str ->
- i64 ptr = str; // Convert array pointer to a i64 for the native call
- libc.write(fd,ptr,str#);
+ writeOff(fd,str,0,str#);
};
-// Write all of str to fd.
+// Write a slice, capping len at the remaining bytes. An offset past the end
+// is invalid, including for an empty write. A valid empty slice returns zero.
+// Invalid arguments return -1;
val writeOff = { i32 fd, u8[~] str, u32 off, u32 len ->
- i64 ptr = str; // Convert array pointer to a i64 for the native call
- ptr += off; // Skip offset
- // Cap at available length
- if( len > str# - off ) len = str# - off;
- // Do the write
- libc.write(fd,ptr,len);
+ if( off > str# ) return libc.set_errno(libc.EINVAL);
+ if( len > str# - off ) len = str# - off;
+ i64 ptr = str;
+ ptr += off;
+ i64 done = 0;
+ while( done < len ) {
+ i64 rez = libc.write(fd,ptr+done,len-done);
+ if( rez < 0 ) {
+ if( libc.errno() == libc.EINTR ) continue;
+ return -1;
+ }
+ if( rez == 0 ) {
+ libc.set_errno(libc.EIO);
+ return -1;
+ }
+ done += rez;
+ }
+ return done;
};
// Return stdin as a u8[].
val stdin = { -> read(0); };
+// One byte, without Enter or echo. Restores the previous terminal mode before
+// returning. Files/pipes also work. Returns 0..255, or -1 with libc.errno() set
+// to libc.EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8 sequences are multiple bytes.
+val getch = { -> libc.simple_getch(); };
+
// Create file for writing. Return fd, or -1 and errno has the error code
val create = { u8[~] str ->
i64 ptr = sys.aryu8.str0(str);
diff --git a/src/main/smp/sys/libc.smp b/src/main/smp/sys/libc.smp
index 17ee65cb..7122bbd8 100644
--- a/src/main/smp/sys/libc.smp
+++ b/src/main/smp/sys/libc.smp
@@ -2,17 +2,17 @@
// libc bindings:
// https://www.man7.org/linux/man-pages/man2/<libc call>.2.html
-// The buffers are int-versions of zero-terminated strings.
-// int buf = aryu8.str0("some_file");
+// The buffers are i64 addresses of zero-terminated strings.
+// i64 buf = aryu8.str0("some_file");
// buf mode perm-> fd
{ i64 i32 i32 -> i32 } open = "C";
// buf perm -> fd
{ i64 i32 -> i32 } creat = "C";
// fd buf len -> len
-{ i32 i64 u32 -> i32 } write = "C";
+{ i32 i64 i64 -> i64 } write = "C";
// fd buf len -> len
-{ i32 i64 u32 -> i32 } read = "C";
+{ i32 i64 i64 -> i64 } read = "C";
// fd -> err
{ i32 -> i32 } close = "C";
// path -> err
@@ -27,12 +27,29 @@ i32 O_TRUNC = 1024; // 04000 on x86_64-pc-cygwin
i32 STDIN = 0;
i32 STDOUT = 1;
i32 STDERR = 2;
+i32 EOF = -1; // simple_getch sets errno to EOF on end-of-input.
// status
{ i32 -> i32 } _exit = "C";
+
// errno is not an ordinary C data symbol; read it through the native runtime.
{ i32 } simple_errno = "C";
val errno = { -> simple_errno(); };
+{ i32 -> i32 } simple_set_errno = "C";
+// Set errno and return -1, matching the native I/O error convention.
+val set_errno = { i32 error -> simple_set_errno(error); };
+
+i32 EINTR = "C";
+i32 EIO = "C";
+i32 EINVAL = "C";
+i32 EOVERFLOW = "C";
+
+// Read one byte from stdin, without <enter> or echo. Restores the previous
+// terminal mode before returning. Files and pipes also work. Returns 0..255,
+// or -1 with errno set to EOF or an OS error. Ctrl-C is byte 3; escape/UTF-8
+// sequences are multiple bytes.
+{ i32 } simple_getch = "C";
+
// addr len prot flags fd off -> void*
{ i64 i64 i32 i32 i32 i32 -> i64 } mmap = "C";
// mmap flags
diff --git a/src/main/smp/sys/rt.smp b/src/main/smp/sys/rt.smp
new file mode 100644
index 00000000..d62b4def
--- /dev/null
+++ b/src/main/smp/sys/rt.smp
@@ -0,0 +1,3 @@
+// Run Time support: "rt" has the same meaning as Java's rt.jar or C's crt.o.
+// Native entry helpers are compiled and linked independently of ordinary sys
+// imports, so programs without library imports can use the same startup code.
diff --git a/src/main/smp/sys/rt/simple_args.smp b/src/main/smp/sys/rt/simple_args.smp
new file mode 100644
index 00000000..3f630e54
--- /dev/null
+++ b/src/main/smp/sys/rt/simple_args.smp
@@ -0,0 +1,22 @@
+// Native startup support, compiled independently and linked with the CRT.
+// Raw native addresses stay i64; only the arrays allocated here are Simple objects.
+{ i64 i64 -> i64 } simple_argv = "C";
+{ i64 -> i64 } strlen = "C";
+{ i64 i64 i64 -> i64 } memcpy = "C";
+
+val simple_args = { i64 argc, i64 argv ->
+ if( argc < 0 || argc > 4294967295 ) return null;
+ // Zero-filled reference arrays require nullable elements. Every slot is
+ // filled before returning; the native entry bridge publishes u8[~][].
+ val args = new u8[~]?[argc];
+ for( i64 i=0; i<argc; i++ ) {
+ i64 src = simple_argv(argv,i);
+ i64 len = strlen(src);
+ if( len < 0 || len > 4294967295 ) return null;
+ val bytes = new u8[len];
+ i64 dst = bytes;
+ memcpy(dst,src,len);
+ args[i] = bytes;
+ }
+ return args;
+};
diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp
new file mode 100644
index 00000000..a414ff45
--- /dev/null
+++ b/src/main/smp/sys/text.smp
@@ -0,0 +1,98 @@
+// Formatting primitives used directly by interpolation and named converters.
+// Keep this module free of interpolation: sys can build itself from source.
+// spec's low byte is the format kind; bits 8..12 are flags: - + space # 0.
+
+val begin = { -> new aryu8(); };
+
+val end = { aryu8 b ->
+ val bytes = new u8[b.len];
+ ary.copy(bytes,0,b.buf,0,b.len);
+ u8[~] result = bytes;
+ result;
+};
+
+val _pad = { !aryu8 b, i64 n, u8 c ->
+ for( i64 i=0; i<n; i++ ) b.add(c);
+ b;
+};
+
+val string = { !aryu8 b, u8[~] s, i64 spec, i64 width, i64 precision ->
+ i64 len=s#;
+ if( precision>=0 && precision<len ) len=precision;
+ i64 pad=width-len;
+ if( !(spec&256) ) _pad(b,pad,' ');
+ for( i64 i=0; i<len; i++ ) b.add(s[i]);
+ if( spec&256 ) _pad(b,pad,' ');
+ b;
+};
+
+val integer = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision ->
+ i64 kind=spec&255;
+ if( kind=='b' ) return string(b,value ? "true" : "false",spec,width,-1);
+ if( kind=='c' ) {
+ if( !(spec&256) ) _pad(b,width-1,' ');
+ u8 c=value;
+ b.add(c);
+ if( spec&256 ) _pad(b,width-1,' ');
+ return b;
+ }
+ bool signed=kind==0 || kind=='d' || kind=='i';
+ i64 base=kind=='o' ? 8 : (kind=='x' || kind=='X' ? 16 : 10);
+ u8[~] alphabet=kind=='X' ? "0123456789ABCDEF" : "0123456789abcdef";
+ val digits=new u8[22]; // Maximum: 22 octal digits for a 64-bit integer.
+ i64 count=0, n=value;
+ // Signed decimal stays nonpositive, including the minimum i64.
+ if( signed && n>0 ) n=-n;
+ if( n==0 && precision!=0 ) digits[count++]='0';
+ while( n!=0 ) {
+ i64 q=0, r=0;
+ if( signed ) { q=n/base; r=q*base-n; }
+ else {
+ // Unsigned division without a u64 type. Halving first makes
+ // the dividend nonnegative; the remainder needs at most one fix.
+ q=((n>>>1)/base)*2;
+ r=n-q*base;
+ if( r>=base ) { q++; r-=base; }
+ }
+ digits[count++]=alphabet[r];
+ n=q;
+ }
+ u8[~] !prefix="";
+ if( signed ) {
+ if( value<0 ) prefix="-";
+ else if( spec&512 ) prefix="+";
+ else if( spec&1024 ) prefix=" ";
+ } else if( (spec&2048) && value!=0 ) {
+ if( kind=='x' ) prefix="0x";
+ if( kind=='X' ) prefix="0X";
+ }
+ i64 zeros=precision>count ? precision-count : 0;
+ // Alternate octal always begins with a zero, including precision-zero 0.
+ if( kind=='o' && (spec&2048) && zeros==0 && (count==0 || digits[count-1]!='0') ) zeros=1;
+ i64 pad=width-prefix#-zeros-count;
+ bool left=(spec&256)!=0;
+ bool zero=(spec&4096)!=0 && precision<0 && !left;
+ if( !left && !zero ) _pad(b,pad,' ');
+ b.addAry(prefix);
+ if( zero ) _pad(b,pad,'0');
+ _pad(b,zeros,'0');
+ while( count>0 ) b.add(digits[--count]);
+ if( left ) _pad(b,pad,' ');
+ b;
+};
+
+// Only conversion of binary floating-point values is delegated to libc.
+// The fixed signature accepts a validated format and returns a byte string.
+{ f64 i64 i64 i64 -> u8[~] } simple_text_float = "C";
+val floating = { !aryu8 b, f64 value, i64 spec, i64 width, i64 precision ->
+ b.addAry(simple_text_float(value,spec,width,precision));
+};
+
+// Explicitly named, typed conversions share the same formatting routines.
+val int = { i64 x -> end(integer (begin(),x, 0 ,0,-1)); };
+val flt = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
+val f32 = { f32 x -> end(floating(begin(),x,'g',0,-1)); };
+val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); };
+val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); };
+val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); };
+val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); };
diff --git a/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/src/test/java/com/seaofnodes/simple/BoundaryTest.java
index 10b7517e..0b05dc52 100644
--- a/src/test/java/com/seaofnodes/simple/BoundaryTest.java
+++ b/src/test/java/com/seaofnodes/simple/BoundaryTest.java
@@ -20,7 +20,7 @@ public class BoundaryTest {
.driver(CodeGen.Phase.TypeCheck);
assertEquals("7",Eval2.eval(factory,0));
}
- String src="struct Counter { int x; int y; int z; new Counter={int n -> while(x<n) { int old=x; y=x+1; x=y+1; z=old; }}; }; val c=new Counter(arg); return c.x*100+c.z;";
+ String src="struct Counter { int x; int y; int z; new Counter={int n -> while(x<n) { int old=x; y=x+1; x=y+1; z=old; }}; }; val c=new Counter(((arg<<32)>>32)); return c.x*100+c.z;";
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched);
assertEquals("2018",Eval2.eval(code,19));
assertEquals("0",Eval2.eval(code,0));
@@ -53,11 +53,11 @@ public class BoundaryTest {
@Test public void testRecursiveCallback() {
String src="""
val run={int n ->
- val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);};
- val sumPowers={int n,{int int -> int} f -> if(n<=0) return 0; return f(2,n)+sumPowers(n-1,f);};
+ val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,--exponent);};
+ val sumPowers={int n,{int int -> i64} f -> if(n<=0) return 0; return f(2,n)+sumPowers(--n,f);};
return sumPowers(n,power);
};
- return run(arg);
+ return run(((arg<<32)>>32));
""";
for( long seed : new long[]{2,13,123} ) {
CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Opto);
@@ -74,11 +74,11 @@ public class BoundaryTest {
"retain(power); return power(2,n);",
"if(n) { retain(power); while(true) {} } return power(2,n);"
} ) {
- String src="val run={int n -> val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,exponent-1);}; { {int int -> int} -> int } retain=\"C\";"+tail+"}; return run(arg);";
+ String src="val run={int n -> val power={int base,int exponent -> if(exponent<=0) return 1; return base*power(base,--exponent);}; { {int int -> i64} -> int } retain=\"C\";"+tail+"}; return run(((arg<<32)>>32));";
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Opto);
ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null);
assertNotNull(base);
- assertSame(TypeInteger.BOT,base._type);
+ assertSame(TypeInteger.I32,base._type);
code.driver(CodeGen.Phase.TypeCheck);
}
}
diff --git a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
index c2713e4f..166f5c5e 100644
--- a/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
+++ b/src/test/java/com/seaofnodes/simple/BrainFuckTest.java
@@ -63,7 +63,7 @@ val brain_fuck = { ->
return output;
};
""";
- TestC.runC(src,"brain_fuck", brain_fuck, 40);
+ TestC.runC(src,"brain_fuck", brain_fuck,165);
EvalRisc5 R5 = TestRisc5.build( src, "brain_fuck", 0, 25, false);
int trap = R5.step(100000);
diff --git a/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/src/test/java/com/seaofnodes/simple/Chapter04Test.java
index 1132375e..bfde3b2a 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter04Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter04Test.java
@@ -127,14 +127,14 @@ public class Chapter04Test {
public void testBug1() {
CodeGen code = new CodeGen("int a=arg+1; int !b=a; b=1; return a+2; ");
code.parse();
- assertEquals("return (arg+3);", code.print());
+ assertEquals("return ((((arg+1)<<32)>>32)+2);", code.print());
}
@Test
public void testBug2() {
CodeGen code = new CodeGen("int !a=arg+1; a=a; return a; ");
code.parse();
- assertEquals("return (arg+1);", code.print());
+ assertEquals("return (((arg+1)<<32)>>32);", code.print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/src/test/java/com/seaofnodes/simple/Chapter05Test.java
index afdb1071..43057c00 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter05Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter05Test.java
@@ -21,7 +21,7 @@ else {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,(arg+2),(arg-3));", code.print());
+ assertEquals("return ((Phi(Region,(arg+2),(arg-3))<<32)>>32);", code.print());
}
@Test
@@ -65,7 +65,7 @@ else
b=a+1;
return a+b;""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return ((arg<<1)+Phi(Region,2,3));", code.print());
+ assertEquals("return ((((Phi(Region,arg,(((arg+1)<<32)>>32))+1)<<32)>>32)+Sar);", code.print());
}
@Test
@@ -80,7 +80,7 @@ else
a=b+1;
return a+b;""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return ((Phi(Region,(arg<<1),arg)+arg)+Phi(Region,4,5));", code.print());
+ assertEquals("return ((((Phi(Region,arg,(((arg+2)<<32)>>32))+1)<<32)>>32)+(((Phi(Region,(((arg+1)<<32)>>32),2)+Phi(Region,Sar,arg))<<32)>>32));", code.print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/src/test/java/com/seaofnodes/simple/Chapter07Test.java
index 7094c0d5..20786571 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter07Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter07Test.java
@@ -35,7 +35,7 @@ if(arg){}else{
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,1,Phi(Loop,1,(Phi_a+1)));", code.print());
+ assertEquals("return Phi(Region,1,Phi(Loop,1,(((Phi_a+1)<<32)>>32)));", code.print());
}
@Test
@@ -55,7 +55,7 @@ while(i < arg) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,Phi(Loop,Phi_sum,(Phi_sum+Phi(Loop,0,(Phi_j+1)))));", code.print());
+ assertEquals("return Phi(Loop,0,Phi(Loop,Phi_sum,(((Phi_sum+Phi(Loop,0,(((Phi_j+1)<<32)>>32)))<<32)>>32)));", code.print());
}
@Test
@@ -90,7 +90,7 @@ while(a < 10) {
return b;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,2,(Phi(Region,Phi_b,4)+1));", code.print());
+ assertEquals("return Phi(Loop,2,(((Phi(Region,Phi_b,4)+1)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -107,7 +107,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -123,7 +123,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -165,7 +165,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -181,7 +181,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -198,7 +198,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -215,7 +215,7 @@ while(a < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,(Phi_a+3));", code.print());
+ assertEquals("return Phi(Loop,1,((((((Phi_a+1)<<32)>>32)+2)<<32)>>32));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/src/test/java/com/seaofnodes/simple/Chapter08Test.java
index 376e472b..249beb11 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter08Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter08Test.java
@@ -46,7 +46,7 @@ while(arg < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(Phi_a+1),Phi_a));", code.print());
+ assertEquals("return Phi(Loop,1,Phi(Region,Phi_a,(((Phi_a+1)<<32)>>32),Phi_a));", code.print());
assertTrue(code.ctrl() instanceof CProjNode);
}
@@ -185,7 +185,7 @@ while(arg < 10) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,Phi(Loop,1,(Phi_a+1)),Add);", code.print());
+ assertEquals("return Phi(Region,Phi(Loop,1,(((Phi_a+1)<<32)>>32)),Sar);", code.print());
assertTrue(code.ctrl() instanceof RegionNode);
}
@@ -201,7 +201,7 @@ while(1) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return (Phi(Loop,1,Add)+1);", code.print());
+ assertEquals("return (((Phi(Loop,1,Sar)+1)<<32)>>32);", code.print());
assertTrue(code.ctrl() instanceof CProjNode prj && prj._idx==1 );
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/src/test/java/com/seaofnodes/simple/Chapter09Test.java
index 7025b98a..6be01487 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter09Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter09Test.java
@@ -41,7 +41,7 @@ else {
return x;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,(arg<<1),(Shl+1));", code.print());
+ assertEquals("return Phi(Region,(arg<<1),(((((Shl<<32)>>32)+1)<<32)>>32));", code.print());
Assert.assertEquals( "2", Eval2.eval(code, 1));
Assert.assertEquals("23", Eval2.eval(code, 11));
}
@@ -182,7 +182,7 @@ while(arg) {
return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(-(Phi_a+3)));", code.print());
+ assertEquals("return Phi(Loop,0,(((-(Phi_a+3))<<32)>>32));", code.print());
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java
index eadaf9fe..559def2f 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter10Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java
@@ -74,7 +74,8 @@ public class Chapter10Test {
CodeGen code = new CodeGen("""
val f = { int n -> (n+1)&7; };
int x = 0;
- if (!!(arg && (x=f(arg)))) return x;
+ int n=arg;
+ if (!!(n && (x=f(n)))) return x;
return -1;
""").driver(CodeGen.Phase.TypeCheck);
assertEquals("-1",Eval2.eval(code,0));
@@ -181,7 +182,7 @@ struct s0 { int v0; };
arg=0+new s0.0;
""");
try { code.parse(); fail(); }
- catch( Exception e ) { assertEquals("Expected an identifier, found 'null'",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Expected an identifier, found '0'",e.getMessage()); }
}
@Test
@@ -196,7 +197,7 @@ while (arg) {
return bar.a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return Test.Bar; ]", code.print());
+ assertEquals("Stop[ return ((Phi(Loop,0,(Sar+2))<<32)>>32); return Test.Bar; ]", code.print());
}
@Test
@@ -318,7 +319,7 @@ while( i.x < i.len ) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return Test.Iter; ]", code.print());
+ assertEquals("Stop[ return Phi(Loop,0,(((Phi_sum+((Phi(Loop,0,(((Sar+1)<<32)>>32))<<32)>>32))<<32)>>32)); return Test.Iter; ]", code.print());
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/src/test/java/com/seaofnodes/simple/Chapter12Test.java
index c6817308..8b7c5eef 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter12Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter12Test.java
@@ -127,7 +127,7 @@ N n = new N(null);
return n.next;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i64 !m; }",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Argument #0 isa null, but must be a *Test.M {i32 !m; }",e.getMessage()); }
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter13Test.java b/src/test/java/com/seaofnodes/simple/Chapter13Test.java
index 214bc69e..70cfe25e 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter13Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter13Test.java
@@ -35,7 +35,7 @@ while( prime <= arg ) {
return primeCount;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi_primeCount,(Phi_primeCount+1))));", code.print());
+ assertEquals("return Phi(Region,0,Phi(Loop,1,Phi(Region,Phi_primeCount,Phi_primeCount,(((Phi_primeCount+1)<<32)>>32))));", code.print());
assertEquals("0", Eval2.eval(code, 1)); // No primes 1 or below
assertEquals("1", Eval2.eval(code, 2)); // 2
assertEquals("2", Eval2.eval(code, 3)); // 2, 3
@@ -116,7 +116,7 @@ if (v1) {
return v0.f;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,.f,2);", code.print());
+ assertEquals("return ((Phi(Region,((.f<<32)>>32),2)<<32)>>32);", code.print());
}
@@ -134,7 +134,7 @@ while (arg) v.f = 2;
return i;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return arg;", code.print());
+ assertEquals("return ((arg<<32)>>32);", code.print());
}
@Test
@@ -151,7 +151,7 @@ while(1) {
return v.f;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,2);", code.print());
+ assertEquals("return ((Phi(Loop,0,2)<<32)>>32);", code.print());
for( int arg : new int[]{-1,0,1,3,100} )
assertEquals("0",Eval2.eval(code,arg));
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java b/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
index e058a822..9a3bb87a 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter14FloatTest.java
@@ -12,8 +12,10 @@ public class Chapter14FloatTest {
{"int rounded=arg/2.5; return rounded;", "6", "-6"},
{"int rounded=0; rounded=arg/2.5; return rounded;", "6", "-6"},
{"i8 rounded=arg*16.0; return rounded;", "16", "-16"},
- {"int rounded=1e100; return rounded;", "9223372036854775807", "9223372036854775807"},
- {"int rounded=-1e100; return rounded;", "-9223372036854775808", "-9223372036854775808"},
+ {"int rounded=1e100; return rounded;", "-1", "-1"},
+ {"int rounded=-1e100; return rounded;", "0", "0"},
+ {"i64 rounded=1e100; return rounded;", "9223372036854775807", "9223372036854775807"},
+ {"i64 rounded=-1e100; return rounded;", "-9223372036854775808", "-9223372036854775808"},
{"int rounded=1e300*1e300-1e300*1e300; return rounded;", "0", "0"},
{"f32 rounded=arg+16777216.0; return rounded;", "1.6777232E7", "1.6777199E7"}
};
diff --git a/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/src/test/java/com/seaofnodes/simple/Chapter15Test.java
index c2071cb6..97c3af46 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter15Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter15Test.java
@@ -157,7 +157,7 @@ return a;
""");
code.driver(CodeGen.Phase.FinalizeIR);
assertEquals("return []*Test._A?;", code.print());
- assertEquals("*Test._A {i64 !i; }?[ null,null]", Eval2.eval(code, 0));
+ assertEquals("*Test._A {i32 !i; }?[ null,null]", Eval2.eval(code, 0));
}
@Test
@@ -312,7 +312,7 @@ flt is = new int[2];
return is;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Type *[]i64 is not of declared type flt",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Type *[]i32 is not of declared type flt",e.getMessage()); }
}
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter16Test.java b/src/test/java/com/seaofnodes/simple/Chapter16Test.java
index 2942cfee..09bd9793 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter16Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter16Test.java
@@ -77,7 +77,7 @@ return z.x;
struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; };
!_LLI? !head = null;
while( arg ) {
- head = new _LLI(head,arg);
+ int n=arg; head = new _LLI(head,n);
arg = arg-1;
}
if( !head ) return 0;
@@ -107,7 +107,7 @@ struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int a ->
}; };
!_LLI? !head = null;
while( arg ) {
- head = new _LLI(head,arg);
+ int n=arg; head = new _LLI(head,n);
arg = arg-1;
}
if( !head ) return 0;
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
index 98197b77..12e54193 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -409,7 +409,7 @@ var b = arg ? new _Bar : null;
return b ? b.x++ + b.x++ : -1;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print());
+ assertEquals("return Phi(Region,((((.x+1)<<32)>>32)+ld_x),-1);", code.print());
assertEquals("-1", Eval2.eval(code, 0));
assertEquals("1", Eval2.eval(code, 1));
@@ -423,7 +423,7 @@ var b = arg ? new _Bar;
return b ? b.x++ + b.x++ : -1;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Region,((.x<<1)+1),-1);", code.print());
+ assertEquals("return Phi(Region,((((.x+1)<<32)>>32)+ld_x),-1);", code.print());
assertEquals("-1", Eval2.eval(code, 0));
assertEquals("1", Eval2.eval(code, 1));
}
@@ -475,7 +475,7 @@ for( int i=0; i<arg; i++ )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -488,7 +488,7 @@ for( ; i<arg; i++ )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -503,7 +503,7 @@ for( int i=0; ; i++ ) {
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -516,7 +516,7 @@ for( int i=0; i<arg; )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+Phi(Loop,0,(Phi_i+1))));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+Phi(Loop,0,(((Phi_i+1)<<32)>>32)))<<32)>>32));", code.print());
assertEquals("3", Eval2.eval(code, 3));
assertEquals("45", Eval2.eval(code, 10));
}
@@ -578,8 +578,9 @@ return new _A(new !B?[0]).array_of_b;
CodeGen code = new CodeGen("""
struct _LLI { !_LLI? !next; int i; new _LLI = { !_LLI? !n, int ii -> next=n; i=ii; }; };
!_LLI? !head = null;
-while( arg-- )
- head = new _LLI(head,arg);
+while( arg-- ) {
+ int n=arg; head = new _LLI(head,n);
+}
int sum=0;
var ptr = head; // A read-only ptr, to be assigned from read-only next fields
for( ; ptr; ptr = ptr.next )
@@ -587,7 +588,7 @@ for( ; ptr; ptr = ptr.next )
return sum;
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("return Phi(Loop,0,(Phi_sum+.i));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+.i)<<32)>>32));", code.print());
assertEquals("45", Eval2.eval(code, 10));
}
@@ -618,8 +619,8 @@ for( int j=0; j<nprimes; j++ )
return rez;
""");
code.driver(CodeGen.Phase.FinalizeIR).driver(CodeGen.Phase.Schedule);
- assertEquals("return []i64;", code.print());
- assertEquals("i64[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20));
+ assertEquals("return []i32;", code.print());
+ assertEquals("i32[ 2,3,5,7,11,13,17,19]",Eval2.eval(code, 20));
}
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/src/test/java/com/seaofnodes/simple/Chapter18Test.java
index ecb2b934..b1caae96 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter18Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter18Test.java
@@ -49,9 +49,9 @@ public class Chapter18Test {
}
@Test public void testFunctionLocalConstantChains() {
- String src = "val f = { int x -> x ? f(x-1)*305420988+x/305420988 : 1; }; "+
- "val g = { int x -> x ? g(x-1)/305420988+x*305420988 : 1; }; "+
- "return f(arg)+g(arg);";
+ String src = "val f = { int x -> int prev=x-1; x ? f(prev)*305420988+x/305420988 : 1; }; "+
+ "val g = { int x -> int prev=x-1; x ? g(prev)/305420988+x*305420988 : 1; }; "+
+ "int n=arg; return f(n)+g(n);";
CodeGen code = new CodeGen(src);
code.driver(CodeGen.Phase.Schedule,"riscv","SystemV");
var owners = new java.util.IdentityHashMap<CFGNode,Boolean>();
@@ -173,8 +173,8 @@ return x2;
};
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return { sq}; return (Parm_x(sq,i64)*x); ]", code.print());
- assertEquals("{ ptr i64 -> i64 #[ 3]}", Eval2.eval(code, 3));
+ assertEquals("Stop[ return { sq}; return (Parm_x(sq,i32)*x); ]", code.print());
+ assertEquals("{ ptr i32 -> i64 #[ 3]}", Eval2.eval(code, 3));
}
@Test
@@ -184,10 +184,10 @@ return x2;
var _sq = { int x ->
x*x;
};
-return _sq(arg)+_sq(3);
+int n=arg; return _sq(n)+_sq(3);
""");
code.driver(Phase.LocalSched);
- assertEquals("return ((arg*arg)+9);", code.print());
+ assertEquals("return ((((arg<<32)>>32)*Sar)+9);", code.print());
assertEquals("13", Eval2.eval(code, 2));
}
@@ -200,8 +200,8 @@ int cnt=1; // Global scope; exactly one of these
return { -> cnt++; }; // Escape global-scope variable is OK
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return { ptr -> i64 #[ 3]}; return .cnt; ]", code.print());
- assertEquals("{ ptr -> i64 #[ 3]}", Eval2.eval(code, 0));
+ assertEquals("Stop[ return { ptr -> i32 #[ 3]}; return .cnt; ]", code.print());
+ assertEquals("{ ptr -> i32 #[ 3]}", Eval2.eval(code, 0));
}
// Function scope test
@@ -244,7 +244,7 @@ var fcn = arg ? { int x -> x*x; } : { int x -> x+x; };
return fcn(3);
""");
code.driver(CodeGen.Phase.FinalizeIR);
- assertEquals("Stop[ return #2; return (Parm_x($fun3,i64,3)*x); return (Parm_x($fun4,i64,3)<<1); ]", code.print());
+ assertEquals("Stop[ return #2; return (Parm_x($fun3,i32,3)*x); return (Parm_x($fun4,i32,3)<<1); ]", code.print());
assertEquals("6", Eval2.eval(code, 0));
assertEquals("9", Eval2.eval(code, 1));
}
@@ -252,10 +252,10 @@ return fcn(3);
// Recursive factorial test
@Test
public void testFcn5() {
- CodeGen code = new CodeGen("val _fact = { int x -> x <= 1 ? 1 : x*_fact(x-1); }; return _fact(arg);");
+ CodeGen code = new CodeGen("val _fact = { int x -> int prev=x-1; x <= 1 ? 1 : x*_fact(prev); }; int n=arg; return _fact(n);");
code.driver(CodeGen.Phase.TypeCheck);
String ir = code.print();
- assertTrue(ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,arg,(x-1))*#2)); ]") ||
+ assertTrue(ir,ir.equals("Stop[ return #2; return Phi(Region,1,(Parm_x(_fact,((arg<<32)>>32),(((x-1)<<32)>>32))*#2)); ]") ||
ir.equals("Stop[ return Phi(Region,1,(arg*#2)); return Phi(Region,1,(Parm_x(_fact,(arg-1),(x-1))*#2)); ]") ||
ir.equals("Stop[ return Phi(Region,1,(arg*Phi(Region,1,((arg-1)*#2)))); return Phi(Region,1,(Parm_x(_fact,(Sub-1),(x-1))*#2)); ]"));
assertEquals( "1", Eval2.eval(code, 0));
@@ -270,7 +270,7 @@ return fcn(3);
// recursive result, but the floating base case requires a floating mode.
@Test
public void testFcn5Float() {
- CodeGen code = new CodeGen("val _fact = { int x -> x <= 1 ? 1.0 : x*_fact(x-1); }; return _fact(arg);");
+ CodeGen code = new CodeGen("val _fact = { int x -> int prev=x-1; x <= 1 ? 1.0 : x*_fact(prev); }; int n=arg; return _fact(n);");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals("1.0", Eval2.eval(code, 0));
assertEquals("1.0", Eval2.eval(code, 1));
@@ -281,7 +281,7 @@ return fcn(3);
@Test
public void testFcn5Minus() {
- CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1; }; return _neg(arg);");
+ CodeGen code = new CodeGen("val _neg = { int x -> int prev=x-1; x ? -_neg(prev) : 1; }; int n=arg; return _neg(n);");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals( "1", Eval2.eval(code, 0));
assertEquals("-1", Eval2.eval(code, 1));
@@ -324,14 +324,14 @@ return 2;
{int -> int}? !_i2i = null;
var _id = {{int->int} f-> return f;};
for(;;) {
- if (_i2i) return _i2i(arg);
+ if (_i2i) { int n=arg; return _i2i(n); }
var _x = {int i-> return i;};
arg = _x(3);
_i2i = _id(_x);
}
""");
code.driver(Phase.LocalSched);
- assertEquals("return Phi(Loop,arg,3);", code.print());
+ assertEquals("return ((Phi(Loop,arg,3)<<32)>>32);", code.print());
assertEquals("3", Eval2.eval(code, 0));
}
@@ -407,7 +407,7 @@ for(;;) {
return 0;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Might be null calling { ptr i64 -> i64 #[ 3]}?",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Might be null calling { ptr i32 -> i32 #[ 3]}?",e.getMessage()); }
}
@@ -419,7 +419,7 @@ val f = { int i, int j -> return i+j; };
return f();
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Expecting 3 arguments, but found 1",e.getMessage()); }
+ catch( Exception e ) { assertEquals("Expecting 2 arguments, but found 0",e.getMessage()); }
}
@@ -457,9 +457,9 @@ return g();
public void testFcnMutRec() {
CodeGen code = new CodeGen(
"""
-val is_even = { int x -> x ? is_odd (x-1) : true ; };
-val is_odd = { int x -> x ? is_even(x-1) : false; };
-return is_even(arg);
+val is_even = { int x -> int prev=x-1; x ? is_odd (prev) : true ; };
+val is_odd = { int x -> int prev=x-1; x ? is_even(prev) : false; };
+int n=arg; return is_even(n);
""");
code.driver(CodeGen.Phase.TypeCheck);
assertEquals("1", Eval2.eval(code, 0));
@@ -496,11 +496,11 @@ for(;;) {
{{int->int}->{int->int}}! _f2f = {{int->int} f->return f;};
val _o = _i2i_noInline;
if (arg) _i2i_noInline = null;
-if (_i2i_noInline) return _i2i_noInline(arg);
+if (_i2i_noInline) { int n=arg; return _i2i_noInline(n); }
return _f2f(_o)(1);
""");
code.driver(Phase.LocalSched);
- assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i64); ]", code.print());
+ assertEquals("Stop[ return Phi(Region,#2,#2); return Parm_i(_i2i_noInline,i32); ]", code.print());
assertEquals("1", Eval2.eval(code, 2));
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/src/test/java/com/seaofnodes/simple/Chapter19Test.java
index 50ca8922..50fecdfd 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter19Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter19Test.java
@@ -32,14 +32,14 @@ public class Chapter19Test {
case "u16" -> arg & 0xffffL;
case "i16" -> (long)(short)arg;
case "u32" -> arg & 0xffffffffL;
- case "i32" -> (long)(int)arg;
+ case "i32", "int" -> (long)(int)arg;
case "f32" -> (double)(float)arg;
case "flt" -> (double)arg;
default -> arg;
};
String msg = type+" array="+array+" arg="+arg;
String printed = array
- ? (type.equals("u8") ? ""+(char)(long)(Long)expected : (type.equals("int") ? "i64" : type)+"[ "+expected+"]")
+ ? (type.equals("u8") ? ""+(char)(long)(Long)expected : (type.equals("int") ? "i32" : type)+"[ "+expected+"]")
: "Test.S{x="+expected+"}";
assertEquals(msg,printed,Eval2.eval(code,arg));
}
@@ -118,8 +118,8 @@ val hashCode = { !String self ->
hashCode(new String("Hello, World!"));
""";
CodeGen code = new CodeGen(src).driver(Phase.LocalSched);
- assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[]))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[])))); ]", code.print());
- assertEquals("4029215624828139541", Eval2.eval(code, 2));
+ assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(((((Phi_hash<<5)-Phi_hash)+.[])<<32)>>32))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(((((Phi_hash<<5)-Phi_hash)+.[])<<32)>>32)))); ]", code.print());
+ assertEquals("1498789909", Eval2.eval(code, 2));
}
@Test
@@ -251,7 +251,7 @@ hashCode(new String("Hello"));
int arg1 = arg + 1;
return arg1 / arg;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (div,(inc,arg),arg);", code.print());
+ assertEquals("return (div,(sari,(shli,(inc,arg))),arg);", code.print());
}
@Test
@@ -262,7 +262,7 @@ int arg1 = arg + 1;
return arg1 * arg;
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (mul,(inc,arg),arg);", code.print());
+ assertEquals("return (mul,(sari,(shli,(inc,arg))),arg);", code.print());
}
@Test
@@ -272,7 +272,7 @@ int a = arg;
return a + 2.0;
"""
).driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (addf,(cvtf,arg),2.0f);", code.print());
+ assertEquals("return (addf,(cvtf,(sari,(shli,arg))),2.0f);", code.print());
}
@Test
@@ -287,7 +287,7 @@ else {
}
return a;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return Phi(Region,(addi,arg),(addi,arg));", code.print());
+ assertEquals("return (sari,(shli,Phi(Region,(addi,arg),(addi,arg))));", code.print());
}
@Test
@@ -302,7 +302,7 @@ else
a=b+1;
return a+b;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (add,(add,Phi(Region,(shli,arg),arg),arg),Phi(Region,4,5));", code.print());
+ assertEquals("return (add,(sari,(shli,(inc,Phi(Region,arg,(sari,(shli,(addi,arg))))))),(sari,(shli,(add,Phi(Region,(sari,(shli,(inc,arg))),2),Phi(Region,sari,arg)))));", code.print());
}
@Test
@@ -314,7 +314,7 @@ for( int i=0; i<arg; i++ )
sum += i;
return sum;""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return Phi(Loop,0,(add,Phi_sum,Phi(Loop,0,(inc,Phi_i))));", code.print());
+ assertEquals("return Phi(Loop,0,(sari,(shli,(add,Phi_sum,Phi(Loop,0,(sari,(shli,(inc,Phi_i))))))));", code.print());
}
@Test
@@ -331,21 +331,21 @@ return new _S;""");
public void testLea1() {
CodeGen code = new CodeGen("int x = arg/3; return arg+x+7;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
public void testLea2() {
CodeGen code = new CodeGen("int x = arg/3; return arg+x*4+7;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
public void testLea3() {
CodeGen code = new CodeGen("int x = arg/3; return x*4+arg;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (lea,arg,(div,arg,3));", code.print());
+ assertEquals("return (lea,arg,(sari,(shli,(div,arg,3))));", code.print());
}
@Test
@@ -359,7 +359,7 @@ return new _S;""");
public void testAlloc3() {
CodeGen code = new CodeGen("int[] !xs = new int[3]; xs[arg]=1; return xs[arg&1]+3;");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return .[];", code.print());
+ assertEquals("return (addi,.[]);", code.print());
}
@Test
@@ -376,7 +376,7 @@ for( int i=0; i<ary#-1; i++ )
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return .[];", code.print());
+ assertEquals("return (add,.[],(muli,.[]));", code.print());
}
@Test
@@ -479,7 +479,7 @@ val _fcn = arg ? { int x -> x*x; } : { int x -> x+x; };
return _fcn(2)*10 + _fcn(3);
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,Parm_x($fun3,[2-3]),x); return (shli,Parm_x($fun4,[2-3])); ]", code.print());
+ assertEquals("Stop[ return (add,#2,(muli,#2)); return (mul,(sext32,Parm_x($fun3,[2-3])),sext32); return (shli,(sext32,Parm_x($fun4,[2-3]))); ]", code.print());
}
@Test
@@ -487,9 +487,9 @@ return _fcn(2)*10 + _fcn(3);
CodeGen code = new CodeGen(
"""
val _sq = { int x -> x*x; };
-return _sq(arg) + _sq(3);
+int n=arg; return _sq(n) + _sq(3);
""");
code.driver(Phase.LocalSched,"x86_64_v2", "SystemV");
- assertEquals("return (addi,(mul,arg,arg));", code.print());
+ assertEquals("return (addi,(mul,(sari,(shli,arg)),sari));", code.print());
}
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index f5eb2b36..273d76ba 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -10,6 +10,7 @@ import static org.junit.Assert.*;
public class Chapter20Test {
@Test public void testAllocatorTopPhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.topPhi(); }
+ @Test public void testIndependentFunctions() { com.seaofnodes.simple.codegen.RegAllocTestSupport.independentFunctions(); }
@Test public void testNeverReturnAllocation() {
String[] sources = {
@@ -141,7 +142,8 @@ val _sqrt = { int x ->
guess = next;
}
};
-return _sqrt(arg) + _sqrt(arg+2);
+int x=arg, y=arg+2;
+return _sqrt(x) + _sqrt(y);
""";
testCPU(src,"x86_64_v2", "win64" ,38,null);
testCPU(src,"riscv" , "SystemV",19,null);
@@ -216,9 +218,9 @@ for( int i=0; i<ary#-1; i++ )
ary[i+1] += ary[i];
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
- testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
- testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"x86_64_v2", "SystemV",-1,"return (add,.[],(muli,.[]));");
+ testCPU(src,"riscv" , "SystemV",9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"arm" , "SystemV",13,"return (add,.[],(mul,.[],1000));");
}
@@ -259,7 +261,7 @@ val hashCode = { !String self ->
""";
// Return the public hashCode wrapper; keep its helper private.
- testCPU(src,"x86_64_v2", "SystemV",12,null);
+ testCPU(src,"x86_64_v2", "SystemV",28,null);
testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",6,null);
}
@@ -267,7 +269,7 @@ val hashCode = { !String self ->
@Test
public void testCast() {
String src = "struct Bar { int x; }; var b = arg ? new Bar; return b ? b.x++ + b.x++ : -1;";
- testCPU(src,"x86_64_v2", "SystemV",3,null);
+ testCPU(src,"x86_64_v2", "SystemV",5,null);
testCPU(src,"riscv" , "SystemV",6,null);
testCPU(src,"arm" , "SystemV",6,null);
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
index e15c2581..c6d558eb 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java
@@ -17,7 +17,7 @@ import static org.junit.Assert.*;
public class Chapter21Test {
@Test public void testFloatToInteger() throws IOException {
- String src = "val float_to_int={ flt x -> int i=x; return i; };"+
+ String src = "val float_to_int={ flt x -> i64 i=x; return i; };"+
"val float_to_u8={ flt x -> u8 i=x; return i; };";
TestC.runC(src,"floatToInteger","",-1);
for( double x : new double[]{0.0,-0.0,6.92,-6.92,255.75,-257.75,
@@ -138,9 +138,10 @@ public class Chapter21Test {
String src = """
val sum = { int n ->
if( n<=0 ) return 1;
- return sum(n-1)+n;
+ int prev=n-1;
+ return sum(prev)+n;
};
- val run = { int n -> return sum(n)+sum(n+1); };
+ val run = { int n -> int next=n+1; return sum(n)+sum(next); };
""";
assertTrue("Ordinary calls must survive optimization",checkArmCalls(src,"run",4,27)>0);
}
@@ -157,7 +158,7 @@ public class Chapter21Test {
buf=buf2;
};
val add = { int e ->
- if( len>=buf# ) grow(buf#*2);
+ if( len>=buf# ) { int size=buf#*2; grow(size); }
buf[len++]=e;
return self;
};
@@ -323,9 +324,9 @@ for( int i=0; i<ary#-1; i++ )
ary[i+1] += ary[i];
return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6
""";
- testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);");
- testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));");
- testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"x86_64_v2", "SystemV",-1,"return (add,.[],(muli,.[]));");
+ testCPU(src,"riscv" , "SystemV",9,"return (add,.[],(mul,.[],1000));");
+ testCPU(src,"arm" , "SystemV",13,"return (add,.[],(mul,.[],1000));");
}
@Test
@@ -384,7 +385,7 @@ val hashCode = { !String self ->
};
""";
// Return the public hashCode wrapper; keep its helper private.
- testCPU(src,"x86_64_v2", "SystemV",12,null);
+ testCPU(src,"x86_64_v2", "SystemV",28,null);
testCPU(src,"riscv" , "SystemV",8,null);
testCPU(src,"arm" , "SystemV",6,null);
}
@@ -423,7 +424,7 @@ val hashCode = { !String self ->
: (self._hashCode = _hashCodeString(self));
};
""";
- TestC.runC(src, "stringHash", "",12);
+ TestC.runC(src, "stringHash", "",28);
}
@Test public void testLoop2() {
@@ -439,9 +440,9 @@ while(true) {
}
return i;
""";
- testCPU(src,"x86_64_v2", "win64" ,0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));");
- testCPU(src,"riscv" , "SystemV",0,"return (!=0)( Phi(Loop,0,Phi(Region,(!=0),(!=0),0)) + #1 );");
- testCPU(src,"arm" , "SystemV",0,"return (!=0)(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)));");
+ testCPU(src,"x86_64_v2", "win64" ,0,"return (!=0)(sari,(shli,(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)))));");
+ testCPU(src,"riscv" , "SystemV",0,"return (!=0)( ( ( Phi(Loop,0,Phi(Region,(!=0),(!=0),0)) + #1 ) << #32 ) >> #1056 );");
+ testCPU(src,"arm" , "SystemV",0,"return (!=0)(asri,(lsli,(inc,Phi(Loop,0,Phi(Region,(!=0),(!=0),0)))));");
}
@Test public void testNewtonExport() throws IOException {
@@ -530,7 +531,7 @@ val sieve = { int N ->
String sprimes = sb.p("]").toString();
// Compile, link against native C; expect the above string of primes to be printed out by C
- TestC.runC(src, "sieve", sprimes, 186);
+ TestC.runC(src, "sieve", sprimes,344);
// Evaluate on RISC5 emulator; expect return of an array of primes in
// the simulated heap.
diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index e9a668a7..9bebee23 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -18,7 +18,7 @@ import static org.junit.Assert.*;
public class Chapter22Test {
@Test public void testNarrowCReturnsEmulated() {
- String[] types = {"i8","u8","i16","u16","i32","u32","int"};
+ String[] types = {"i8","u8","i16","u16","i32","u32","i64","int"};
long[] values = {0xa5a5a5a500000000L,0xa5a5a5a50000007fL,0xa5a5a5a500000080L,
0xa5a5a5a500007fffL,0xa5a5a5a500008000L,0xa5a5a5a57fffffffL,
0xa5a5a5a580000000L,0xa5a5a5a5ffffffffL};
@@ -35,7 +35,7 @@ public class Chapter22Test {
long expected = switch(i) {
case 0 -> (byte)value; case 1 -> value&255;
case 2 -> (short)value; case 3 -> value&65535;
- case 4 -> (int)value; case 5 -> value&0xffffffffL;
+ case 4, 7 -> (int)value; case 5 -> value&0xffffffffL;
default -> value;
};
byte[] image = new byte[1<<16];
@@ -70,7 +70,7 @@ public class Chapter22Test {
{i8} c_i8="C"; {u8} c_u8="C";
{i16} c_i16="C"; {u16} c_u16="C";
{i32} c_i32="C"; {u32} c_u32="C";
- {int} c_i64="C";
+ {i64} c_i64="C";
val test_i8={->return c_i8();};
val test_u8={->return c_u8();};
val test_i16={->return c_i16();};
@@ -344,7 +344,7 @@ val _sum = { i32[~] is -> // final array
return _sum(is);
""";
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
- assertEquals("return Phi(Loop,0,(Phi_sum+.[]));", code.print());
+ assertEquals("return Phi(Loop,0,(((Phi_sum+.[])<<32)>>32));", code.print());
assertEquals("14", Eval2.eval(code, 0));
}
@@ -354,7 +354,8 @@ return _sum(is);
String src =
"""
// Echo stdin to stdout.
-return sys.io.p( sys.io.stdin() );
+val input=sys.io.stdin();
+return input ? sys.io.p(input) : -1;
""";
TestC.runSF(src,"echo", "",0);
diff --git a/src/test/java/com/seaofnodes/simple/Chapter23Test.java b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
index df02d3b4..89e41efa 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter23Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter23Test.java
@@ -279,7 +279,7 @@ return _s.peek('q');
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
assertEquals("return Phi(Region,0,1);", code.print());
assertEquals("1", Eval2.eval(code, 0));
- testCPU(src,"x86_64_v2", "win64",23,null);
+ testCPU(src,"x86_64_v2", "win64",31,null);
};
@Test
diff --git a/src/test/java/com/seaofnodes/simple/Chapter24Test.java b/src/test/java/com/seaofnodes/simple/Chapter24Test.java
index 2bc2ce7f..f4c4f12f 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter24Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter24Test.java
@@ -20,9 +20,9 @@ public class Chapter24Test {
@Test public void testRecursiveWidening() {
String[] srcs = {
// Parm(x) must widen even though the recursive result is constant.
- "val f = { int x, int limit -> x >= limit ? 0 : f(x+1,limit); }; return f(0,arg);",
+ "val f = { int x, int limit -> x >= limit ? 0 : f(++x,limit); }; int limit=arg; return f(0,limit);",
// The growing result has its own cycle, which never crosses Parm(x).
- "val f = { int x -> x <= 0 ? 0 : f(x-1)+1; }; return f(3);"
+ "val f = { int x -> x <= 0 ? 0 : f(--x)+1; }; return f(3);"
};
for( int i=0; i<srcs.length; i++ )
for( long seed : new long[]{0,42,126} ) {
@@ -32,6 +32,7 @@ public class Chapter24Test {
}
@Test public void testColdLoopSelfConflict() { com.seaofnodes.simple.codegen.RegAllocTestSupport.coldLoopSelfConflict(); }
+ @Test public void testTiedLoopBackedges() { com.seaofnodes.simple.codegen.RegAllocTestSupport.tiedLoopBackedges(); }
@Test public void testPrintingDeadFunction() throws Exception {
var code = new CodeGen("return 0;").parse();
@@ -106,7 +107,7 @@ return (sq_noInline(2) <= score < sq_noInline(3))
? 0 // Expected
: 1; // Error
""";
- TestC.runSF( src, "stacked_r_3", "", 2);
+ TestC.runSF( src, "stacked_r_3", "",0);
// Evaluate on RISC5 emulator
EvalRisc5 R5 = TestRisc5.build( src, "stacked_r_3", 0, 2, false);
diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 5f615282..67e7f982 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -142,8 +142,9 @@ public class Chapter25Test {
CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}),
"Client","""
val _other={int n -> n-1;};
- val inc={int n -> n+1;};
- return Lib.apply_noInline(inc,arg);
+ val inc={int n -> n+=1; return n;};
+ int n=arg;
+ return Lib.apply_noInline(inc,n);
""",987654321,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
var clz=(com.seaofnodes.simple.type.TypeStruct)Parser.TYPES.get("class:Lib");
int hidden=((com.seaofnodes.simple.type.TypeFunPtr)clz.field("_hidden_noInline")._t).fidx();
@@ -169,7 +170,7 @@ public class Chapter25Test {
String[] sources={
"val _spin={ -> while(1) {} return 1; }; _spin(); return 2;",
// Two callers require cloning, including the non-exiting arm.
- "val _spin={int x -> if(x) while(1) {} return 7; }; return _spin(arg)+_spin(arg);"
+ "val _spin={int x -> if(x) while(1) {} return 7; }; int n=arg; return _spin(n)+_spin(n);"
};
for( int i=0; i<sources.length; i++ ) {
for( long seed : new long[]{1,123,987654321} ) {
@@ -191,6 +192,9 @@ public class Chapter25Test {
i8 small="C";
f64 fraction="C";
{int} bump="C";
+ val extern_matches={i32 x -> counter==x;};
+ val extern_less={i32 x -> x<counter;};
+ val extern_seven={ -> counter==7;};
val test_global={ ->
val before=counter;
counter=before;
@@ -471,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, char, collections, and array utilities.
+ // Sys depends on io, libc, text, char, collections, and array utilities.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/scan"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
@@ -496,7 +500,7 @@ public class Chapter25Test {
@Test
public void testHelloWorldDriver() throws Exception {
String out = "build/objs/helloWorldDriver/";
- Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","src/test/smp/examples/A_helloWorld.smp"});
+ Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","docs/examples/A_helloWorld.smp"});
assertTrue(Files.isRegularFile(Path.of(out,"A_helloWorld.o")));
}
@@ -507,7 +511,7 @@ public class Chapter25Test {
try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) {
System.setOut(capture);
Simple.main(new String[]{"-L",SYS_BLDDIR,"-o","build/objs/helloWorldDriverLibFile/",
- "src/test/smp/examples/A_helloWorld.smp"});
+ "docs/examples/A_helloWorld.smp"});
} finally {
System.setOut(saved);
}
@@ -554,7 +558,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testBubbles() throws IOException {
- String src = Files.readString( Path.of("src/test/smp/examples/BubbleSort.smp"));
+ String src = Files.readString( Path.of("docs/examples/BubbleSort.smp"));
String exe = TestC.compile(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("[-17, 2, 3, 999]\n",
@@ -575,7 +579,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testCapitalize() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/Capitalize.smp"));
+ String src = Files.readString(Path.of("docs/examples/Capitalize.smp"));
String exe = TestC.compile(src,"Capitalize",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("Hello world\n",TestC.exec(exe,"hello world"));
@@ -588,7 +592,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testDijkstra() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/Dijkstra.smp"));
+ String src = Files.readString(Path.of("docs/examples/Dijkstra.smp"));
String matrix = "[0, 2, 0, 6, 0, 2, 0, 3, 8, 5, 0, 3, 0, 0, 7, 6, 8, 0, 0, 9, 0, 5, 7, 9, 0]";
String exe = TestC.compile(src,"Dijkstra",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
@@ -603,7 +607,7 @@ return rez < buf# ? 0 : sys.libc._exit(-2);
@Test
public void testFileIO() throws IOException {
- String src = Files.readString(Path.of("src/test/smp/examples/FileIO.smp"));
+ String src = Files.readString(Path.of("docs/examples/FileIO.smp"));
String exe = TestC.compile(src,"FileIO",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION,null,null,-1);
assertEquals("File I/O succeeded\n",TestC.exec(exe));
diff --git a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
index dae64fb0..8f1d1ee8 100644
--- a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
+++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
@@ -188,7 +188,7 @@ public class ClassInitDependenciesTest {
"B","int value=A.value;"),"A -> B -> A");
cycle("A",Map.of("A","int value=get(); val get={ -> B.value; };",
"B","int value=A.value;"),"A -> B -> A");
- cycle("A",Map.of("A","int value=0; if(arg) value=B.value;",
+ cycle("A",Map.of("A","int value=0; if(((arg<<32)>>32)) value=B.value;",
"B","int value=A.value;"),"A -> B -> A");
cycle("A",Map.of("A","val f=arg ? { -> B.value; } : { -> 0; }; int value=f();",
"B","int value=A.value;"),"A -> B -> A");
@@ -237,7 +237,7 @@ public class ClassInitDependenciesTest {
}
@Test public void testRecursionAndNativeCalls() throws Exception {
- CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(n-1) : B.value; }; int value=f(arg);",
+ CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(--n) : B.value; }; int value=f(((arg<<32)>>32));",
"B","int value=arg|1;"),123);
assertTrue(touches(code,new ClassInitDependencies(code),"Root/A","Root/B"));
code=compile("A",Map.of("A","{int} native=\"C\"; return native();"),123);
@@ -386,8 +386,8 @@ public class ClassInitDependenciesTest {
String message=ex.getMessage();
assertCycle(message,"Root.A -> Root.B -> Root.A");
// Inlining must keep Helper's source identity, not substitute A.
- assertTrue(message,message.contains("Helper.smp:2:4"));
- assertTrue(message,message.contains(" B.value;\n ---^"));
+ assertTrue(message,message.contains("Helper.smp:2:5"));
+ assertTrue(message,message.contains(" B.value;\n ____^"));
if( name.endsWith("noInline") ) {
assertTrue(message,message.contains("A.smp:1:31"));
assertTrue(message,message.contains("int value=Helper.read_noInline();"));
@@ -401,7 +401,7 @@ public class ClassInitDependenciesTest {
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Parse);
CompUnit cu=code._compunits.get("Test");
int pos=src.substring(0,src.indexOf('1')).getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
- assertEquals("Test.smp:2:9: error: example\n return 1;\n ---------------^",SourceError.format(cu,pos,"example"));
+ assertEquals("Test.smp:2:16: error: example\n return 1;\n _______________^",SourceError.format(cu,pos,"example"));
assertEquals("Test.smp:3:1: error: example\n \n ^",SourceError.format(cu,src.getBytes(java.nio.charset.StandardCharsets.UTF_8).length,"example"));
}
@@ -414,8 +414,8 @@ public class ClassInitDependenciesTest {
@Test public void testInitializationMemory() throws Exception {
String[] sources={
"int value=B.value+B.value;",
- "int value; if(arg) value=B.value; else value=B.value+1; value=value+B.value;",
- "int value; if(arg) value=B.value; value=value+B.value;"
+ "int value; if(((arg<<32)>>32)) value=B.value; else value=B.value+1; value=value+B.value;",
+ "int value; if(((arg<<32)>>32)) value=B.value; value=value+B.value;"
};
int[] counts={1,2,2};
for( long seed : new long[]{1,123,987654321} )
diff --git a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
index 285d4293..b9d65b1c 100644
--- a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
+++ b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -23,7 +23,7 @@ public class CompoundAssignmentTest {
@Test public void testLocals() {
for (String op : OPS)
for (long arg : new long[]{-8, 0, 5, 64}) {
- long expected = apply(op, arg, 3);
+ long expected = (int)apply(op, arg, 3);
check("int x=arg; int r=(x " + op + " 3); return x*10+r;",
arg, expected*11);
}
@@ -31,7 +31,7 @@ public class CompoundAssignmentTest {
@Test public void testRhsOnce() {
for (String op : OPS) {
- long expected = apply(op, 5, 1);
+ long expected = (int)apply(op, 5, 1);
check("int x=arg; int count=0; int r=(x " + op +
" (count=count+1)); return count*1000+x*10+r;", 5, 1000+expected*11);
}
@@ -42,7 +42,7 @@ public class CompoundAssignmentTest {
check("u8 x=255; x ^= 256; return x;", 0, 255);
check("int x=1; int y=3; x |= y <<= 2; return x*100+y;", 0, 1312);
check("int x=-8; x >>= 65; return x;", 0, -4);
- check("int x=-8; x >>>= 65; return x;", 0, Long.MAX_VALUE-3);
+ check("int x=-8; x >>>= 65; return x;", 0, -4);
}
@Test public void testExistingOperators() {
@@ -73,7 +73,7 @@ public class CompoundAssignmentTest {
@Test public void testFields() {
for (String op : OPS) {
- long expected = apply(op, -8, 3);
+ long expected = (int)apply(op, -8, 3);
check("struct S { int x; }; !S !s=new S; s.x=arg; int r=(s.x " +
op + " 3); return s.x*10+r;", -8, expected*11);
}
@@ -81,7 +81,7 @@ public class CompoundAssignmentTest {
@Test public void testArrayAddressAndRhsOnce() {
for (String op : OPS) {
- long expected = apply(op, 5, 1);
+ long expected = (int)apply(op, 5, 1);
check("int[] !a=new int[2]; a[0]=arg; int i=0; int count=0; " +
"int r=(a[i++] " + op + " (count=count+1)); " +
"return i*1000+count*100+a[0]*10+r;", 5, 1100+expected*11);
diff --git a/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
index e544fcad..1eb64610 100644
--- a/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
+++ b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
@@ -30,7 +30,7 @@ public class ConstructionGroupTest {
@Test public void testCyclesAndIndependentAllocations() {
String src = parent("val detached = new Child(null); payload = n+read(detached); child = new Child(self);")+
- "val p = new Parent(arg); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;";
+ "val p = new Parent(((arg<<32)>>32)); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;";
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
assertEquals("17",Eval2.eval(code,7));
@@ -44,7 +44,7 @@ public class ConstructionGroupTest {
// Child's own slots are initialized, even though its parent remains
// partial. Reading that slot does not complete the parent reference.
String src = parent("child = new Child(self); payload = child.parent==null;")+
- "return new Parent(arg).payload;";
+ "return new Parent(((arg<<32)>>32)).payload;";
assertEquals("0",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0));
}
@@ -73,7 +73,7 @@ public class ConstructionGroupTest {
}
@Test public void testAliasesAndLoopBackedges() {
- String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(arg).child.value;";
+ String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(((arg<<32)>>32)).child.value;";
assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),3));
rejects(parent("payload = 1; child = new Child(self); Child? !c = null; while(n--) { if(c) read(c); c = child; }")+
"return 0;","ordinary function");
@@ -116,7 +116,7 @@ public class ConstructionGroupTest {
head = p;
};
};
- val root = new Root(arg);
+ val root = new Root(((arg<<32)>>32));
return root.value + root.head.value;
""";
CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
diff --git a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
index 71a281b5..5a20388b 100644
--- a/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
+++ b/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java
@@ -28,7 +28,7 @@ public class EscapeFlowTest {
// Start memory rather than a frozen declaration constant.
@Test public void testUnknownCallerCallback() {
CodeGen code = new CodeGen("""
- val _hidden_noInline = { int n -> return n+2; };
+ val _hidden_noInline = { int n -> int result=n+2; return result; };
val apply_noInline = { {int->int} f,int n -> return f(n); };
return apply_noInline;
""").parse();
@@ -42,16 +42,16 @@ public class EscapeFlowTest {
assertTrue(callback.in(1) instanceof ProjNode);
TypeFunPtr type = (TypeFunPtr)callback._type;
assertTrue(XInt.isHigh(type.fidxs()));
- assertSame(TypeInteger.BOT,apply.ret().expr()._type);
+ assertSame(TypeInteger.I32,apply.ret().expr()._type);
// Only <clinit> and apply escape; forwarding #self must not expose _hidden.
assertEquals("Stop[ return { apply_noInline}; return #2; ]",code.print());
}
@Test public void testNativeCallbackRoundTrip() {
CodeGen code = new CodeGen("""
- val _callback_noInline = { int n -> return n+1; };
+ val _callback_noInline = { int n -> int result=n+1; return result; };
{ {int->int} -> {int->int} } bounce = "C";
- return bounce(_callback_noInline)(arg);
+ return bounce(_callback_noInline)(((arg<<32)>>32));
""").driver(CodeGen.Phase.TypeCheck);
FunNode callback = named(code,"_callback_noInline");
assertTrue(callback.unknownCallers());
@@ -87,7 +87,7 @@ public class EscapeFlowTest {
@Test public void testPublicAllocator() {
CodeGen code = new CodeGen("""
struct Box { int n; new Box = { int x -> n=x; }; };
- return new Box(arg).n;
+ return new Box(((arg<<32)>>32)).n;
""").parse();
FunNode allocator = named(code,"Test.Box.<new>");
int[] helpers = XInt.EMPTY;
diff --git a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
index 66fdacc3..bdb71fc6 100644
--- a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
+++ b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
@@ -7,7 +7,7 @@ import static org.junit.Assert.*;
public class FinalConstructionTest {
private static final String LOOP = """
- val step={int x -> x*2+1;};
+ val step={i64 x -> x*2+1;};
struct C {
val x=1;
new C={int n ->
@@ -16,7 +16,7 @@ public class FinalConstructionTest {
};
val get={->x;};
};
- return new C(arg).get();
+ return new C(((arg<<32)>>32)).get();
""";
@Test public void testFinalConstructorFields() throws java.io.IOException {
@@ -27,10 +27,10 @@ public class FinalConstructionTest {
// Writes are ordinary Stores and may remain inside the loop.
String[] sources={
"struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;",
- "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;",
- "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;",
+ "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(((arg<<32)>>32)).x;",
+ "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(((arg<<32)>>32)).x;",
"struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;",
- "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];",
+ "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(((arg<<32)>>32)).x[0];",
"struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;"
};
String[] results={"2","8","4","2.5","98","3"};
diff --git a/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
index d8f7ef34..155ebac3 100644
--- a/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
+++ b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java
@@ -68,8 +68,8 @@ public class IncrementalBuildTest {
@Test public void testSourceChangeAndCycle() throws Exception {
Path root=fixture("bundle-");
Path src=root.resolve("src"), obj=root.resolve("obj");
- write(src,"App/A","val value={int n -> n ? B.value(n-1) : 1; };",1000);
- write(src,"App/B","val value={int n -> n ? A.value(n-1) : 5; };",1000);
+ write(src,"App/A","val value={int n -> n ? B.value(--n) : 1; };",1000);
+ write(src,"App/B","val value={int n -> n ? A.value(--n) : 5; };",1000);
write(src,"App/Main","return A.value(1);",1000);
compile(src,obj,"App/A");
assertEquals("One build timestamp for the cycle",Files.getLastModifiedTime(obj.resolve("App/A.o")),
@@ -82,7 +82,7 @@ public class IncrementalBuildTest {
assertNull(code._compunits.get("App/A")._src);
assertNull(code._compunits.get("App/B")._src);
- write(src,"App/B","val value={int n -> n ? A.value(n-1) : 7; };",4000);
+ write(src,"App/B","val value={int n -> n ? A.value(--n) : 7; };",4000);
code=client(src,obj);
assertEquals("7",Eval2.eval(code,0));
assertNotNull(code._compunits.get("App/A")._src);
diff --git a/src/test/java/com/seaofnodes/simple/Int32Test.java b/src/test/java/com/seaofnodes/simple/Int32Test.java
new file mode 100644
index 00000000..bea7c116
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/Int32Test.java
@@ -0,0 +1,94 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.cpus.arm.arm;
+import com.seaofnodes.simple.node.cpus.riscv.riscv;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeStruct;
+import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class Int32Test {
+ @Test public void testAliasAndLayout() {
+ new CodeGen("struct Pair { int x; int y; }; return new Pair;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertSame(TypeInteger.I32,Parser.INIT_TYPES.get("int"));
+ var pair=(TypeStruct)Parser.TYPES.get("Test.Pair");
+ assertSame(TypeInteger.I32,pair.field("x")._t);
+ assertEquals(8,pair.size());
+ assertEquals(4,Math.abs(pair.offset(1)-pair.offset(0)));
+ }
+
+ @Test public void testNarrowAssignmentsAndUpdates() {
+ for(String source:new String[]{
+ "int x=arg; return x;",
+ "int x=0; x=arg; return x;",
+ "struct S { int x; }; val s=new S; s.x=arg; return s.x;",
+ "val a=new int[2]; a[1]=arg; return a[1];"
+ }) {
+ var code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ for(long x:new long[]{0,2147483647L,2147483648L,-2147483649L,Long.MAX_VALUE,Long.MIN_VALUE})
+ assertEquals(source,Integer.toString((int)x),Eval2.eval(code,x));
+ }
+ var increment=new CodeGen("int x=arg; x++; return x;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("-2147483648",Eval2.eval(increment,2147483647));
+ var compound=new CodeGen("int x=arg; x+=2; return x;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("-2147483647",Eval2.eval(compound,2147483647));
+ }
+
+ @Test public void testWideExpressionsAndInference() {
+ for(String source:new String[]{
+ "int x=2147483647; return x+1;",
+ "var x=2147483647; x++; return x;",
+ "val x=2147483648; return x;",
+ "i64 x=2147483648; return x;"
+ }) assertEquals(source,"2147483648",Eval2.eval(new CodeGen(source).driver(CodeGen.Phase.TypeCheck),0));
+ }
+
+ @Test public void testLoopBoundSurvivesWidening() {
+ for(String type:new String[]{"int","u32"}) {
+ String source="val f_noInline={"+type+" x -> x;}; "+type+" x=0; while(x<arg) x++; return f_noInline(x);";
+ for(long seed:new long[]{1,123,456}) {
+ var code=new CodeGen(source,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,7));
+ }
+ }
+ var high=new CodeGen("val f_noInline={u32 x -> x;}; u32 x=2147483646; while(x<arg) x++; return f_noInline(x);")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("2147483650",Eval2.eval(high,2147483650L));
+ var wrap=new CodeGen("val f_noInline={u32 x -> x;}; u32 x=4294967294; while(x) x++; return f_noInline(x);")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(wrap,0));
+ }
+
+ @Test public void testNativeLayoutAndParameters() throws IOException {
+ TestC.runC("""
+ val int_echo={int x -> x;};
+ val int_wrap={int x -> x++; return x;};
+ val wide_echo={i64 x -> x;};
+ val narrow_sum={i8 a,u8 b,i16 c,u16 d,int e,u32 f,int g,u32 h -> a+b+c+d+e+f+g+h;};
+ struct Pair { int x; int y; };
+ val int_pair={ -> val p=new Pair; p.x=-1; p.y=2147483647; return p; };
+ val int_array={ -> val a=new int[3]; a[0]=-1; a[1]=2147483647; a[2]=-2147483648; return a; };
+ ""","int32","ok\n",-1);
+ }
+
+ @Test public void testNarrowParameterBackends() throws IOException {
+ long input=0xa5a5a5a580008080L;
+ String[] types={"i8","u8","i16","u16","int","u32"};
+ long[] expected={(byte)input,input&255,(short)input,input&65535,(int)input,input&0xffffffffL};
+ for(int i=0; i<types.length; i++) {
+ String source="val probe={"+types[i]+" x -> x;};";
+ var a=TestArm64.build("probe",source,0,-1,false);
+ a.regs[arm.X0]=input;
+ assertEquals(0,a.step(1000));
+ assertEquals(types[i],expected[i],a.regs[arm.X0]);
+ var r=TestRisc5.build(source,"probe",0,-1,false);
+ r.regs[riscv.A0]=input;
+ assertEquals(0,r.step(1000));
+ assertEquals(types[i],expected[i],r.regs[riscv.A0]);
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/MergeSortTest.java b/src/test/java/com/seaofnodes/simple/MergeSortTest.java
index f5d3d146..b177de23 100644
--- a/src/test/java/com/seaofnodes/simple/MergeSortTest.java
+++ b/src/test/java/com/seaofnodes/simple/MergeSortTest.java
@@ -18,8 +18,9 @@ public class MergeSortTest {
"""
val merge_sort = { int[] a ->
int[] !b = new int[a#];
- _copy_array (a, 0, a#, b);
- _split_merge(a, 0, a#, b);
+ int len=a#;
+ _copy_array (a, 0, len, b);
+ _split_merge(a, 0, len, b);
};
val _split_merge = { int[] !b, int begin, int end, int[] a ->
@@ -57,16 +58,16 @@ val _copy_array = { int[] !a, int begin, int end, int[] b ->
for( int prime : primes )
sb.p(prime).p(", ");
String sprimes = sb.p("]").toString();
- TestC.runC(src, "sort", sprimes, 34);
+ TestC.runC(src, "sort", sprimes,70);
EvalRisc5 R5 = TestRisc5.build( src, "merge_sort", 0, 36, false);
// Allocate an array of primes, filled in reverse order
int ps = R5._heap;
- R5._heap += (primes.length+1)*8; // 25 primes, and length
+ R5._heap += (primes.length+1)*4; // 25 primes, and length
R5.st4(ps,primes.length);
for( int i=0; i<primes.length; i++ )
- R5.st8(ps+(primes.length-1-i+1)*8,primes[i]);
+ R5.st4(ps+(primes.length-1-i+1)*4,primes[i]);
// Pass array base when calling `merge_sort`
R5.regs[riscv.A0] = ps; // Array base to be sorted
@@ -75,7 +76,7 @@ val _copy_array = { int[] !a, int begin, int end, int[] b ->
assertEquals(primes.length,R5.ld4s(ps));
for( int i=0; i<primes.length; i++ )
- assertEquals(primes[i], R5.ld8(ps+(i+1)*8));
+ assertEquals(primes[i], R5.ld4s(ps+(i+1)*4));
// TODO: replace with ARM Eval
Chapter21Test.testCPU(src,"arm", "SystemV",36,null);
diff --git a/src/test/java/com/seaofnodes/simple/MinusNodeTest.java b/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
index f7fbde03..1b120e6e 100644
--- a/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/MinusNodeTest.java
@@ -57,7 +57,7 @@ public class MinusNodeTest {
}
@Test public void testRecursiveFloat() {
- CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(x-1) : 1.0; }; return _neg(arg);")
+ CodeGen code = new CodeGen("val _neg = { int x -> x ? -_neg(--x) : 1.0; }; return _neg(((arg<<32)>>32));")
.driver(CodeGen.Phase.TypeCheck);
for( int i=0; i<4; i++ )
assertEquals((i&1)==0 ? "1.0" : "-1.0",Eval2.eval(code,i));
diff --git a/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
new file mode 100644
index 00000000..2b7eea78
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
@@ -0,0 +1,191 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ParserDiagnosticTest {
+ private static Parser.ParseException rejected(String source) {
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.Parse);
+ throw new AssertionError("Accepted invalid source");
+ } catch( Parser.ParseException e ) {
+ return e;
+ }
+ }
+
+ private static void diagnostic(String source, int line, int column, String message) {
+ var error=rejected(source);
+ assertEquals(message,error.getMessage());
+ assertEquals("Test.smp:"+line+":"+column+": error: "+message
+ +"\n "+source.split("\n",-1)[line-1]+"\n "+"_".repeat(column-1)+"^",error.diagnostic());
+ }
+
+ @Test public void testNumericLiteralErrors() {
+ diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
+ diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
+ diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
+ diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
+ }
+
+ @Test public void testUnclosedCommentsAndStrings() {
+ diagnostic("/* never closed",1,1,"Unclosed block comment");
+ diagnostic("return 0; /*",1,11,"Unclosed block comment");
+ diagnostic("return \"abc\n",1,8,"Unclosed string");
+ diagnostic("return \"abc\\",1,8,"Unclosed string");
+ // A comment at byte zero used to index -1; a final // was not recognized.
+ new CodeGen("/* ok */ return 0; //").driver(CodeGen.Phase.Parse);
+ }
+
+ @Test public void testCharacterLiteralsAndEscapes() {
+ diagnostic("return '';",1,8,"Empty character literal");
+ diagnostic("return 'ab';",1,8,"Character literal must contain exactly one byte");
+ diagnostic("return 'a ';",1,8,"Character literal must contain exactly one byte");
+ diagnostic("return '",1,8,"Unclosed character literal");
+ diagnostic("return 'a\n",1,8,"Unclosed character literal");
+ diagnostic("return '\\q';",1,9,"Unknown character escape \\q");
+ diagnostic("return \"\\q\";",1,9,"Unknown string escape \\q");
+ new CodeGen("return ' ' + '\\'' + '\\\\';").driver(CodeGen.Phase.Parse);
+ }
+
+ @Test public void testDeferredLiteralErrors() {
+ diagnostic("struct S {\n new S={ ->\n val x='ab';\n };\n};",3,9,
+ "Character literal must contain exactly one byte");
+ diagnostic("struct S {\n new S={ ->\n val x=\"abc",3,9,"Unclosed string");
+ diagnostic("for(int i=0; i<1; i+='ab') {}",1,22,
+ "Character literal must contain exactly one byte");
+ diagnostic("struct S {\n new S={ ->\n val x=\"outer %(\"bad\\q\")\";\n };\n};",3,22,
+ "Unknown string escape \\q");
+ }
+
+ @Test public void testIdentifierAndKeywordLocations() {
+ diagnostic("int = 1;",1,5,"Expected an identifier, found '='");
+ diagnostic("int true=1;",1,5,"Expected an identifier, found 'true'");
+ diagnostic("int",1,4,"Expected an identifier, found 'EOF'");
+ diagnostic("return if;",1,8,"Syntax error, expected `an identifier or expression` but found `if`");
+ diagnostic("\n break;",2,3,"No active loop for a break or continue");
+ diagnostic("\n continue;",2,3,"No active loop for a break or continue");
+ }
+
+ @Test public void testFinalAssignmentLocations() {
+ diagnostic("val x=1;\n x=2;",2,3,"Cannot reassign final 'x'");
+ diagnostic("val x=1;\n x++;",2,3,"Cannot reassign final 'x'");
+ diagnostic("val x=1;\n ++x;",2,5,"Cannot reassign final 'x'");
+ }
+
+ @Test public void testParameterAndConstructorLocations() {
+ diagnostic("val f={int x, int x -> x;};",1,19,"Duplicate parameter 'x'");
+ diagnostic("val f={int ^x -> x;};",1,12,"'^' requires a struct parameter");
+ diagnostic("struct S { new Wrong={ -> }; };",1,16,"Constructor name 'Wrong' must match type name 'S'");
+ diagnostic("int? x;",1,4,"Type i32 cannot be null");
+ diagnostic("return new int;",1,12,"Cannot allocate a i32");
+ }
+
+ @Test public void testMemberLocation() {
+ diagnostic("struct S { int x; };\nval s=new S;\nreturn s.missing;",3,10,
+ "Accessing unknown field 'missing' from '*Test.S'");
+ diagnostic("struct S { int x; };\nval s=new S;\nreturn s.;",3,10,"Expected an identifier, found ';'");
+ }
+
+ @Test public void testInitializerAndOperatorLocations() {
+ diagnostic("val x = null;",1,9,"Cannot infer a type from null; declare a nullable type explicitly");
+ diagnostic("u8[] bytes=\"C\";",1,6,"C data binding requires an integer or float type");
+ diagnostic("return 1 < 2 > 3;",1,14,"Mixing relational directions in a chained relational test");
+ var shift=rejected("return 1 << 2.0;");
+ assertTrue(shift.getMessage(),shift.getMessage().startsWith("Cannot '<<'"));
+ assertEquals("Test.smp:1:10: error: "+shift.getMessage()
+ +"\n return 1 << 2.0;\n _________^",shift.diagnostic());
+ }
+
+ @Test public void testCLILexicalErrorsWithoutStackTrace() throws Exception {
+ checkCLI("return 9223372036854775808;",
+ ":1:8: error: Integer literal is outside the signed 64-bit range"
+ +"\n return 9223372036854775808;\n _______^\n");
+ checkCLI("/* missing close",
+ ":1:1: error: Unclosed block comment\n /* missing close\n ^\n");
+ }
+
+ @Test public void testMissingInitializer() {
+ var error=rejected("// declaration\n\nval _validMove() {\n");
+ assertEquals("Syntax error, expected `=expression` but found `(`",error.getMessage());
+ assertEquals("Test.smp:3:15: error: "+error.getMessage()
+ +"\n val _validMove() {\n ______________^",error.diagnostic());
+ }
+
+ @Test public void testFoundIdentifierPointsAtItsStart() {
+ var error=rejected("int x=1\nreturn x;");
+ assertEquals("Test.smp:2:1: error: Syntax error, expected `;` but found `return`"
+ +"\n return x;\n ^",error.diagnostic());
+ }
+
+ @Test public void testEOF() {
+ var error=rejected("return 1");
+ assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`"
+ +"\n return 1\n ________^",error.diagnostic());
+ }
+
+ @Test public void testCRLFTabsAndUTF8() {
+ var error=rejected("// π\r\n\tint x=1\r\n\treturn x;");
+ assertEquals("Test.smp:3:9: error: Syntax error, expected `;` but found `return`"
+ +"\n return x;\n ________^",error.diagnostic());
+ var unicode=rejected("val x=\"π\" return 1;");
+ assertEquals("Test.smp:1:11: error: Syntax error, expected `;` but found `return`"
+ +"\n val x=\"π\" return 1;\n __________^",unicode.diagnostic());
+ }
+
+ @Test public void testDeferredForUpdateLocation() {
+ // The update is parsed after the body, so lexer line counters can be stale.
+ var error=rejected("for(int i=0; i<3; i+=) {\n int x=i;\n}\nreturn 0;");
+ assertEquals("Test.smp:1:22: error: Syntax error, expected `an identifier or expression` but found `)`"
+ +"\n for(int i=0; i<3; i+=) {\n _____________________^",error.diagnostic());
+ }
+
+ @Test public void testCLIReportsSourceWithoutStackTrace() throws Exception {
+ checkCLI("// declaration\nval _validMove() {\n",
+ ":2:15: error: Syntax error, expected `=expression` but found `(`"
+ +"\n val _validMove() {\n ______________^\n");
+ }
+
+ @Test public void testUndefinedNameKeepsItsUseSite() {
+ var error=rejected("val make={ -> val board=new u8[9]; board; };\n"
+ +"val read={ ->\n return board[0];\n};\nreturn read();");
+ assertEquals("Test.smp:3:12: error: Undefined name 'board'"
+ +"\n return board[0];\n ___________^",error.diagnostic());
+ }
+
+ @Test public void testUndefinedAssignmentPointsAtName() {
+ var error=rejected("\n board = 1;");
+ assertEquals("Test.smp:2:5: error: Undefined name 'board'"
+ +"\n board = 1;\n ____^",error.diagnostic());
+ }
+
+ @Test public void testCLIUndefinedNameWithoutStackTrace() throws Exception {
+ checkCLI("// unresolved\nreturn board[0];\n",
+ ":2:8: error: Undefined name 'board'"
+ +"\n return board[0];\n _______^\n");
+ }
+
+ private static void checkCLI(String text, String expected) throws Exception {
+ Path dir=Files.createDirectories(Path.of("build/diagnostics/source with spaces"));
+ Path source=dir.resolve("Bad.smp").toAbsolutePath();
+ Files.writeString(source,text);
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"--norun","-o","build/diagnostics/objects/",source.toString()).start();
+ try {
+ assertTrue("Compiler did not exit",child.waitFor(20,TimeUnit.SECONDS));
+ assertEquals(1,child.exitValue());
+ assertEquals("",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
+ String diagnostic=new String(child.getErrorStream().readAllBytes(),StandardCharsets.UTF_8)
+ .replace("\r\n","\n");
+ assertEquals(source+expected,diagnostic);
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/PartialReadTest.java b/src/test/java/com/seaofnodes/simple/PartialReadTest.java
index a6230b69..427096e5 100644
--- a/src/test/java/com/seaofnodes/simple/PartialReadTest.java
+++ b/src/test/java/com/seaofnodes/simple/PartialReadTest.java
@@ -11,7 +11,7 @@ public class PartialReadTest {
private static String program(String body, String helper) {
return "struct _C { int~ x; int~ y; new _C = { int n -> " + body +
" }; }; val _read_noInline = { _C ^p -> " + helper +
- " }; return new _C(arg).y;";
+ " }; return new _C(((arg<<32)>>32)).y;";
}
private static void rejects(String source, String message) {
try {
@@ -108,7 +108,7 @@ public class PartialReadTest {
_Child child;
new _Parent = { int n -> x=n; child=new _Child(self); };
};
- val p=new _Parent(arg); return p.child.seen+p.child.parent.x;
+ val p=new _Parent(((arg<<32)>>32)); return p.child.seen+p.child.parent.x;
""";
assertEquals("22",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),11));
rejects(src.replace("x=n; child=new _Child(self);","child=new _Child(self); x=n;"),"might not be initialized");
@@ -127,7 +127,7 @@ public class PartialReadTest {
new _C = { int n -> data=new int[1]; data[0]=n; y=_read_noInline(self); };
};
val _read_noInline={ _C ^p -> return p.data[0]; };
- return new _C(arg).y;
+ return new _C(((arg<<32)>>32)).y;
""";
assertEquals("23",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),23));
// A set reference slot is not proof that its referent is initialized.
diff --git a/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java b/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java
new file mode 100644
index 00000000..9086e00c
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/RuntimeArgsTest.java
@@ -0,0 +1,29 @@
+package com.seaofnodes.simple;
+
+import java.io.IOException;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class RuntimeArgsTest extends TestC {
+ @Test public void testNativeArguments() throws IOException {
+ runArgs("""
+ val main={ u8[~][] args ->
+ if(args#!=4 || args[0]#==0) return 1;
+ if(args[1]#!=0) return 2;
+ val words=args[2];
+ if(words#!=9 || words[3]!=' ' || words[8]!='s') return 3;
+ val longArg=args[3];
+ if(longArg#!=4096) return 4;
+ for(int i=0; i<longArg#; i++) if(longArg[i]!='x') return 5;
+ return 0;
+ };
+ ""","runtimeArgs",null,CALL_CONVENTION,"",-1,"","two words","x".repeat(4096));
+ }
+
+ @Test public void testArgumentCopiesAndEmptyVector() throws IOException {
+ String exe="build/objs/runtimeArgsCopy"+(OS.startsWith("Windows") ? ".exe" : "");
+ linkExe(runtimeObject("simple_args"),OS.startsWith("Windows") ? "ms_abi" : "sysv_abi",
+ C_DRIVERS_DIR+"runtime_args.c",null,exe);
+ assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
index 88c11101..447025f6 100644
--- a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
+++ b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
@@ -23,7 +23,7 @@ public class SelectiveLoadingTest {
new CodeGen(src.toString(),obj.toString(),null,"Lib",null,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
CodeGen code = new CodeGen(null,null,new Ary<>(new String[]{obj.toString()}),
- "Use","return Lib.sum_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "Use","return Lib.sum_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(code._compunits.get("Lib/Left")._src);
assertNull(code._compunits.get("Lib/Right")._src);
assertEquals("2000000000000",Eval2.eval(code,0));
@@ -33,11 +33,11 @@ public class SelectiveLoadingTest {
@Test public void testUnreachableImportIsNotARoot() throws Exception {
Path root = library("dead-import-");
Ary<String> paths = new Ary<>(new String[]{root.toString()});
- object(root,paths,"Unused","val run_noInline = { int n -> n+1; };");
+ object(root,paths,"Unused","val run_noInline = { int n -> int result=n+1; return result; };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
val _false_noInline = { -> false; };
- if( _false_noInline() ) return Unused.run_noInline(arg);
+ if( _false_noInline() ) return Unused.run_noInline(((arg<<32)>>32));
return 7;
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
CompUnit unused = code._compunits.get("Unused");
@@ -61,8 +61,8 @@ public class SelectiveLoadingTest {
object(root,paths,"L"+i,"val relay_noInline = { {int->int} f -> L"+(i-1)+".relay_noInline(f); };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
- val _increment_noInline = { int n -> n+1; };
- val _decrement_noInline = { int n -> n-1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
+ val _decrement_noInline = { int n -> int result=n-1; return result; };
val f = L5.relay_noInline(arg ? _increment_noInline : _decrement_noInline);
return f(41);
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
@@ -78,9 +78,9 @@ public class SelectiveLoadingTest {
object(root,paths,"Relay","val relay = { {int->int} f -> f; };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main","""
- val _increment_noInline = { int n -> n+1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
val f = Relay.relay(_increment_noInline);
- return f(arg);
+ return f(((arg<<32)>>32));
""",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(function(code,"relay").summary());
assertEquals("42",Eval2.eval(code,41));
@@ -91,11 +91,11 @@ public class SelectiveLoadingTest {
@Test public void testImportedFunctionField() throws Exception {
Path root = library("function-field-");
Ary<String> paths = new Ary<>(new String[]{root.toString()});
- object(root,paths,"Targets","var chosen = { int n -> n+1; };");
+ object(root,paths,"Targets","var chosen = { int n -> int result=n+1; return result; };");
object(root,paths,"Apply","val run = { int n -> val f = Targets.chosen; return f(n); };");
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main",
- "return Apply.run(arg);",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "return Apply.run(((arg<<32)>>32));",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNull(function(code,"run").summary());
assertEquals("42",Eval2.eval(code,41));
assertEquals("1",Eval2.eval(code,0));
@@ -112,15 +112,15 @@ public class SelectiveLoadingTest {
for( String target : new String[]{"apply","apply_noInline","forward_noInline"} )
for( long seed : new long[]{1,123,987654321} ) {
CodeGen code = new CodeGen(null,root.toString(),paths,"Main",
- "val _increment_noInline = { int n -> n+1; }; "+
+ "val _increment_noInline = { int n -> int result=n+1; return result; }; "+
"val forward_noInline = { {int->int} f,int n -> Apply.apply_noInline(f,n); }; return "+
- (target.equals("forward_noInline") ? target : "Apply."+target)+"(_increment_noInline,arg);",
+ (target.equals("forward_noInline") ? target : "Apply."+target)+"(_increment_noInline,((arg<<32)>>32));",
seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertEquals("42",Eval2.eval(code,41));
assertEquals("1",Eval2.eval(code,0));
}
CodeGen nativeCode = new CodeGen(null,root.toString(),paths,"NativeCallback","""
- val _increment_noInline = { int n -> n+1; };
+ val _increment_noInline = { int n -> int result=n+1; return result; };
val run = { int n -> Apply.apply_noInline(_increment_noInline,n); };
""",123,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
Path driver = root.resolve("driver.c");
@@ -212,7 +212,7 @@ public class SelectiveLoadingTest {
val p={int n -> Work.write_noInline(n);};
val p_noInline={int n -> Work.write_noInline(n);};
""");
- write(src,"Lib/Work","val write_noInline={int n -> n+1;};");
+ write(src,"Lib/Work","val write_noInline={ int n -> int result=n+1; return result;};");
write(src,"Lib/Unused","val f={int n -> n*2;};");
new CodeGen(src.toString(),lib.toString(),null,"Lib",null,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
@@ -221,7 +221,7 @@ public class SelectiveLoadingTest {
Ary<String> paths = new Ary<>(new String[]{lib.toString()});
CodeGen code = new CodeGen(null,app.toString(),paths,"Main",
- "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ "if(arg==0) return -1; return Lib.api.p_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
assertNotNull(function(code,"Lib.Api.p_noInline").summary());
assertNotNull(function(code,"Lib.Work.write_noInline").summary());
assertNotNull(function(code,"Lib.Unused.f").summary());
@@ -233,7 +233,7 @@ public class SelectiveLoadingTest {
assertEquals("6",Eval2.eval(code,5));
code=new CodeGen(null,app.toString(),paths,"Main",
- "val run={int n -> Lib.api.p(n);}; return run(arg);",123,true,TypeInteger.BOT)
+ "val run={int n -> Lib.api.p(n);}; return run(((arg<<32)>>32));",123,true,TypeInteger.BOT)
.driver(CodeGen.Phase.TypeCheck);
assertNull("p requested Api's implementation",function(code,"Lib.Api.p").summary());
assertNotNull("The remaining call needs a signature, not a body",function(code,"Lib.Work.write_noInline").summary());
@@ -255,7 +255,7 @@ public class SelectiveLoadingTest {
// fresh program.
try {
Eval2.eval(new CodeGen(null,app.toString(),paths,"Missing",
- "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT),0);
+ "if(arg==0) return -1; return Lib.api.p_noInline(((arg<<32)>>32));",123,true,TypeInteger.BOT),0);
fail("Missing transitive code must fail before evaluation");
} catch( RuntimeException ex ) { assertTrue(ex.getMessage(),ex.getMessage().contains("Lib/Work")); }
}
diff --git a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index cfc0b7e0..1853aa48 100644
--- a/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -18,9 +18,9 @@ public class SeparateCompilationTest {
val run={int n ->
val power={int base,int exponent ->
if(exponent<=0) return 1;
- return base*power(base,exponent-1);
+ return base*power(base,--exponent);
};
- { {int int -> int} -> int } retain="C";
+ { {int int -> i64} -> int } retain="C";
retain(power);
return power(2,n);
};
@@ -28,14 +28,14 @@ public class SeparateCompilationTest {
new CodeGen(null,lib.toString(),null,"Callbacks",src,123,true,TypeInteger.BOT)
.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{lib.toString()}),
- "Client","return Callbacks.run(arg);",123,true,TypeInteger.BOT)
+ "Client","return Callbacks.run(((arg<<32)>>32));",123,true,TypeInteger.BOT)
.driver(CodeGen.Phase.Opto);
// This assertion inspects a body, not just its saved summary.
ParseAll.loadCompUnit(code,code._compunits.get("Callbacks"));
assertNull(code._compunits.get("Callbacks")._src);
ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null);
assertNotNull(base);
- assertSame("Native code may call the saved callback with any base",TypeInteger.BOT,base._type);
+ assertSame("Native code may call the saved callback with any base",TypeInteger.I32,base._type);
code.driver(CodeGen.Phase.TypeCheck);
}
diff --git a/src/test/java/com/seaofnodes/simple/Simple.java b/src/test/java/com/seaofnodes/simple/Simple.java
index 1f1e8ab0..78b1e76d 100644
--- a/src/test/java/com/seaofnodes/simple/Simple.java
+++ b/src/test/java/com/seaofnodes/simple/Simple.java
@@ -3,6 +3,7 @@ package com.seaofnodes.simple;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
import com.seaofnodes.simple.codegen.CodeGen.Phase;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.print.*;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.Ary;
@@ -34,6 +35,8 @@ Options:
-L path - add an external Simple object search path
--eval - (slowly) evaluate the compiled code in emulator
--run - run the compiled code natively; this is the default
+ --link - build a native executable without running it
+ --norun - emit objects without linking or running
--main - export the top-level initializer as C `main`
--cpu <cpu-name> - use specific CPU (x86_64_v2, riscv, arm)
--abi <abi-name> - use specific ABI variant (SystemV)
@@ -90,10 +93,20 @@ Options:
}
public static void main(String[] args) throws Exception {
+ try {
+ run(args);
+ } catch( Parser.ParseException e ) {
+ System.err.println(e.diagnostic());
+ System.exit(1);
+ }
+ }
+
+ private static void run(String[] args) throws Exception {
String root = null;
String input_filename = null;
boolean do_eval = false;
boolean do_run = true;
+ boolean do_link = false;
boolean do_codegen = false;
boolean emit_main = false;
boolean print_time = false;
@@ -134,9 +147,10 @@ Options:
case "--dump": dump |= 1<<Phase.LastPhase .ordinal() ; break;
case "--dump-after-all": dump |= (2<<Phase.LastPhase .ordinal())-1; break;
case "-S": print_asm = true; break;
- case "--eval": do_eval = true ; do_run = false; break;
+ case "--eval": do_eval = true ; do_run = false; do_link = false; break;
case "--run": do_run = true ; break;
- case "--norun": do_run = false; break;
+ case "--link": do_link = true ; do_run = false; break;
+ case "--norun": do_run = false; do_link = false; break;
case "--main": emit_main = true; break;
case "--dump-size": print_size = true; break;
case "--dump-time": print_time = true; break;
@@ -215,7 +229,7 @@ Options:
}
String outBase = outPath.resolve(srcName).toString();
- if( do_run || print_asm || print_size ) {
+ if( do_run || do_link || print_asm || print_size ) {
if (cpu == null) cpu = TestC.CPU_PORT;
if (abi == null) abi = TestC.CALL_CONVENTION;
do_codegen = true;
@@ -234,7 +248,7 @@ Options:
Ary<String> externPaths = libPaths.isEmpty() ? null : new Ary<>(libPaths.toArray(new String[0]));
CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), externPaths, srcName, src, 456, true, TypeInteger.BOT );
code.driver(do_eval && !do_codegen ? Phase.TypeCheck : Phase.LastPhase,
- cpu,abi, false, emit_main || do_run || do_eval, dump);
+ cpu,abi, false, emit_main || do_run || do_link || do_eval, dump);
if( do_codegen && print_asm )
System.out.println(code.asm());
@@ -253,12 +267,12 @@ Options:
if( print_time )
System.out.printf( "EXECUTION TIME: %.3f sec%n", (System.currentTimeMillis() - t) / 1e3);
}
- if( do_run ) {
+ if( do_run || do_link ) {
if ( !TestC.CPU_PORT.equals( cpu ) || !TestC.CALL_CONVENTION.equals( abi ) )
- throw bad("cannot run code on not native target");
+ throw bad("cannot link or run code on a non-native target");
String exe = TestC.OS.startsWith("Windows") ? outBase+".exe" : outBase;
- String result = TestC.gcc(code, null, null, "", exe);
- System.out.print(result);
+ TestC.linkExe(TestC.linkObjs(code),null,null,exe);
+ if( do_run ) TestC.execInteractive(exe);
}
}
}
diff --git a/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
new file mode 100644
index 00000000..f3491314
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -0,0 +1,138 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class StreamIOTest extends TestC {
+ @Test public void testReadUntilEOF() throws Exception {
+ String src="""
+ val main={ ->
+ val bytes=sys.io.stdin();
+ if(!bytes) return 1;
+ return sys.io.write(1,bytes)==bytes# ? 0 : 2;
+ };
+ """;
+ String exe=compile(src,"streamEcho",new Ary<>(new String[]{RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,-1);
+ for(int size:new int[]{0,8192,8193,65537}) {
+ String input="a\0bc\n".repeat(size/5)+"z".repeat(size%5);
+ Path in=Files.createTempFile(Path.of("build/objs"),"stream-in-",".txt");
+ Path out=Files.createTempFile(Path.of("build/objs"),"stream-out-",".txt");
+ Process child=null;
+ try {
+ Files.writeString(in,input);
+ ProcessBuilder builder=new ProcessBuilder(exe).redirectOutput(out.toFile());
+ if(size!=8193) builder.redirectInput(in.toFile());
+ child=builder.start();
+ if(size==8193) { // Also exercise a real pipe, ending only when its writer closes.
+ child.getOutputStream().write(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ child.getOutputStream().close();
+ }
+ assertTrue(child.waitFor(TEST_TIMEOUT_SECONDS,TimeUnit.SECONDS));
+ assertEquals(0,child.exitValue());
+ assertEquals(input,Files.readString(out));
+ } finally {
+ if(child!=null) child.destroyForcibly();
+ Files.deleteIfExists(in);
+ Files.deleteIfExists(out);
+ }
+ }
+ }
+
+ @Test public void testUnsignedArrayGrowth() throws Exception {
+ runSYS("""
+ val main={ ->
+ val b=new sys.aryu8();
+ val first=b.buf;
+ b.add('a');
+ if(b.buf!=first || b.len!=1) return 1;
+ b.addAry("bcdefghij");
+ if(b.len!=10 || b.buf[0]!='a' || b.buf[9]!='j') return 2;
+ b.undo(4294967295);
+ if(b.len!=0) return 3;
+ b.len=2147483648;
+ b.undo(1);
+ if(b.len!=2147483647) return 4;
+ b.clear();
+ val nums=new sys.aryi64(new i64[0]);
+ nums.add(10).add(20);
+ if(nums.len!=2 || nums.get(0)!=10 || nums.get(1)!=20) return 5;
+ nums.len=4294967295;
+ if(nums.len!=4294967295) return 6;
+ nums.len=2;
+ if(sys.ary.capacity(0,1)!=1) return 7;
+ if(sys.ary.capacity(8,8)!=8 || sys.ary.capacity(8,9)!=16) return 8;
+ if(sys.ary.capacity(2147483648,2147483649)!=4294967295) return 9;
+ if(sys.ary.capacity(4294967295,4294967296)!=0) return 10;
+ if(sys.ary.cmp("abc",4294967295,"",0,4294967295)!=0) return 11;
+ nums.str().add('\\n').write(1);
+ return 0;
+ };
+ ""","unsignedArrayGrowth","[10, 20]\n",-1);
+ }
+
+ @Test public void testShortInterruptedAndFailedIO() throws Exception {
+ String src="""
+ {i32 i64 -> i32} test_io_mode="C";
+ {i64} test_io_count="C";
+ {i32} test_io_status="C";
+ val main={ ->
+ test_io_mode(1,0);
+ val bytes=sys.io.read(700);
+ if(!bytes) return 1;
+ if(bytes#!=24593) return 2;
+ for(u32 i=0; i<bytes#; i++) if(bytes[i]!=i-(i/251)*251) return 3;
+ test_io_mode(1,0);
+ if(sys.io.write(701,bytes)!=bytes# || test_io_count()!=bytes#) return 4;
+ if(test_io_status()) return 5;
+ test_io_mode(1,3);
+ if(sys.io.writeOff(701,bytes,3,5)!=5 || test_io_count()!=5) return 6;
+ if(test_io_status()) return 7;
+ test_io_mode(1,24590);
+ if(sys.io.writeOff(701,bytes,24590,4294967295)!=3) return 8;
+ if(test_io_status()) return 9;
+ test_io_mode(1,0);
+ if(sys.io.writeOff(701,bytes,24593,0)!=0 || test_io_count()!=0) return 10;
+ if(sys.io.writeOff(701,bytes,4294967295,1)!=-1) return 11;
+ if(sys.libc.errno()!=sys.libc.EINVAL || test_io_count()!=0) return 12;
+ test_io_mode(2,0);
+ if(sys.io.read(700)!=null || sys.libc.errno()!=sys.libc.EIO) return 13;
+ test_io_mode(3,0);
+ if(sys.io.write(701,bytes)!=-1 || sys.libc.errno()!=sys.libc.EIO) return 14;
+ if(test_io_count()!=7) return 15;
+ test_io_mode(4,0);
+ if(sys.io.write(701,bytes)!=-1 || sys.libc.errno()!=sys.libc.EIO) return 16;
+ test_io_mode(5,0);
+ if(sys.io.write(701,bytes)!=-1 || test_io_status()) return 17;
+ if(sys.io.read(-1)!=null) return 18;
+ val b=new sys.aryu8().addAry(bytes);
+ test_io_mode(1,0);
+ if(b.write(701)!=b.len || test_io_count()!=b.len || test_io_status()) return 19;
+ sys.io.p("ok\\n");
+ return 0;
+ };
+ """;
+ CodeGen code=new CheckedCodeGen(null,"build/objs",new Ary<>(new String[]{RELEASE_SYS_DIR}),
+ "streamFaults",src,126,true,TypeInteger.BOT);
+ code.driver(CPU_PORT,CALL_CONVENTION,false,true);
+ String exe="build/objs/streamFaults"+(OS.startsWith("Windows") ? ".exe" : "");
+ Ary<String> gcc=new Ary<>(String.class);
+ gcc.add("gcc");
+ gcc.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
+ gcc.add("-Wl,--wrap=read,--wrap=write");
+ gcc.add(C_DRIVERS_DIR+"stream_io.c");
+ gcc.addAll(linkObjs(code));
+ gcc.add(runtimeObject("simple_crt"));
+ gcc.add(runtimeObject("simple_args"));
+ gcc.addAll(new String[]{"-lm","-o",exe});
+ exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
+ assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/StringFormatTest.java b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
new file mode 100644
index 00000000..53a10092
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/StringFormatTest.java
@@ -0,0 +1,158 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.node.CallNode;
+import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class StringFormatTest extends TestC {
+ private static void prints(String body, String expected) throws Exception {
+ runSYS("val main={ -> "+body+" return 0; };", "stringFormat",expected,-1);
+ }
+
+ @Test public void testDefaultsAndNestedStrings() throws Exception {
+ prints("sys.io.p(\"CPU plays %(2+3). %(\"nested %(7)\") %(true) %(1.5)\\n\");",
+ "CPU plays 5. nested 7 1 1.5\n");
+ }
+
+ @Test public void testFormats() throws Exception {
+ prints("sys.io.p(\"%+06d(17)|%#08x(42)|%-5.3s(\"abcdef\")|%.2f(1.25)|%c(65)|%b(false)\\n\");",
+ "+00017|0x00002a|abc |1.25|A|false\n");
+ }
+
+ @Test public void testIntegerLimits() throws Exception {
+ prints("i64 lo=-9223372036854775807-1; sys.io.p(\"%(lo)|%(9223372036854775807)|%u(-1)|%X(-1)\");",
+ "-9223372036854775808|9223372036854775807|18446744073709551615|FFFFFFFFFFFFFFFF");
+ }
+
+ @Test public void testSignedAndUnsignedRadices() throws Exception {
+ long[] values={Long.MIN_VALUE,Long.MIN_VALUE+1,Long.MAX_VALUE,-1,-17,0,1,7,8,9,15,16,17,1L<<32};
+ StringBuilder source=new StringBuilder("val show_noInline={i64 x -> sys.io.p(\"%(x)|%u(x)|%o(x)|%x(x)\\n\"); }; val main={ -> ");
+ StringBuilder expected=new StringBuilder();
+ for(long x:values) {
+ source.append("show_noInline(").append(x==Long.MIN_VALUE ? "(-9223372036854775807-1)" : Long.toString(x)).append(");");
+ expected.append(x).append('|').append(Long.toUnsignedString(x)).append('|')
+ .append(Long.toUnsignedString(x,8)).append('|').append(Long.toUnsignedString(x,16)).append('\n');
+ }
+ source.append("return 0; };");
+ runSYS(source.toString(),"stringFormatRadices",expected.toString(),-1);
+ }
+
+ @Test public void testIntegerPaddingRules() throws Exception {
+ prints("sys.io.p(\"%#.0o(0)|%#.3o(1)|%#x(0)|%-08d(17)|%08.4d(-17)|%+d(0)|% d(0)\");",
+ "0|001|0|17 | -0017|+0| 0");
+ }
+
+ @Test public void testBuilderNumberUsesFormatter() throws Exception {
+ prints("val b=new sys.aryu8(); b.num(-9223372036854775807-1).add('|').num(9223372036854775807); sys.io.p(sys.text.end(b));",
+ "-9223372036854775808|9223372036854775807");
+ }
+
+ @Test public void testFormattingInEvaluatorFromSource() {
+ // No installed sys objects and no native execution: the library builds
+ // from source, and all non-float formatting executes as ordinary IR.
+ String source="""
+ val s="%#08x(arg)|%b(true)|%-4s("ok")|%c(65)";
+ val expected="0x00002a|true|ok |A";
+ if(s#!=expected#) return 1;
+ for(int i=0; i<s#; i++) if(s[i]!=expected[i]) return 2;
+ return 0;
+ """;
+ CodeGen code=new CodeGen("src/main/smp",null,null,"FreshText",source,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,42,10000));
+ }
+
+ @Test public void testEvaluationOrderAndLength() throws Exception {
+ prints("int x=0; val s=\"%(x++)%(x++)%(x++)\"; sys.io.p(\"%(s):%(s#):%(x)|%%|\\%\");",
+ "012:3:3|%|%");
+ prints("val a=new u8[1]; a[0]='A'; sys.io.p(\"%(a)%(a[0]='B')%(a)\");", "A66B");
+ }
+
+ @Test public void testConverters() throws Exception {
+ prints("sys.io.p(sys.text.int(17)); sys.io.p(sys.text.char(65)); sys.io.p(sys.text.bool(true)); sys.io.p(sys.text.flt(1.5));",
+ "17Atrue1.5");
+ prints("struct S { int int=7; }; sys.io.p(\"%((new S).int)\");", "7");
+ }
+
+ @Test public void testForwardReturnTypes() throws Exception {
+ runSYS("val main={ -> sys.io.p(\"%(later())\"); return 0; }; val later={ -> \"ok\"; };",
+ "stringFormatForward","ok",-1);
+ }
+
+ @Test public void testFloatAndAlternateFormats() throws Exception {
+ prints("f32 x=1.25f; sys.io.p(\"%(x)|%f(x)|%.2e(x)|%.2G(x)|%#o(9)|% d(7)|%.0d(0)|%a(1.5)\");",
+ "1.25|1.250000|1.25e+00|1.2|011| 7||0x1.8p+0");
+ runSYS("val format_noInline={ f32 x -> \"%(x)|%f(x)\"; }; val main={ -> sys.io.p(format_noInline(1.25f)); return 0; };",
+ "stringFormatFloat","1.25|1.250000",-1);
+ }
+
+ @Test public void testBufferGrowth() throws Exception {
+ prints("sys.io.p(\"%0100d(17)|%.100f(1.0)\");", "0".repeat(98)+"17|1."+"0".repeat(100));
+ }
+
+ @Test public void testLiteralRemainsConstant() {
+ CodeGen code=new CodeGen("return \"100%% \\%\";").driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof CallNode ? n : null));
+ }
+
+ @Test public void testBytesAndEmptyStrings() throws Exception {
+ prints("val empty=\"%(\"\")\"; val bytes=\"a\\0b\"; sys.io.p(\"%(empty#):%s(bytes):%.2s(bytes):%c(0)\");",
+ "0:a\0b:a\0:\0");
+ prints("val s=\"π\"; sys.io.p(\"%(s):%(s#):%(s[0]):π\");", "π:2:207:π");
+ }
+
+ @Test public void testDeferredParsing() throws Exception {
+ prints("""
+ struct S { u8[~] s; new S={ -> s="%("}) %(3)")"; }; };
+ val s=new S;
+ sys.io.p(s.s);
+ for(int i=0; i<2; sys.io.p("%(i++) %("(")")) { }
+ """, "}) 30 (1 (");
+ }
+
+ @Test public void testImportedFormatter() throws Exception {
+ Path lib=Files.createTempDirectory(Path.of("build/objs"),"text-library-");
+ new CodeGen(null,lib.toString(),new Ary<>(new String[]{RELEASE_SYS_DIR}),"TextLibrary",
+ "val format_noInline={int x -> \"value=%04d(x)\"; };",123,true,TypeInteger.BOT)
+ .driver(CPU_PORT,CALL_CONVENTION,false,false);
+ run("val main={ -> sys.io.p(TextLibrary.format_noInline(7)); return 0; };",
+ "stringFormatImported",new Ary<>(new String[]{lib.toString(),RELEASE_SYS_DIR}),
+ CALL_CONVENTION,null,null,"value=0007",-1);
+ }
+
+ @Test public void testWrongTypesAndReservedNames() {
+ rejects("return \"%d(1.5)\";", "must be a");
+ rejects("return \"%s(17)\";", "must be a");
+ rejects("return \"%(null)\";", "Cannot format");
+ rejects("val f={int int -> int;}; return 0;", "Expected an identifier");
+ rejects("val f={ -> int int=1; return int; }; return 0;", "Expected an identifier");
+ }
+
+ private static void rejects(String source, String expected) {
+ try {
+ new CodeGen(null,null,new Ary<>(new String[]{RELEASE_SYS_DIR}),"Test",source,123,true,TypeInteger.BOT)
+ .driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted invalid source: "+source);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(expected));
+ }
+ }
+
+ @Test public void testMalformedFormats() {
+ for(String format:new String[]{"%name", "%1$d(1)", "%*d(1)", "%n(1)", "%lld(1)", "%"})
+ rejects("return \""+format+"\";", "Expected a string format");
+ rejects("return \"%X.cliff\";", "Expected '('");
+ rejects("return \"%+s(\"x\")\";", "Invalid flags");
+ rejects("return \"%.2c(65)\";", "Precision is not supported");
+ }
+
+ @Test public void testUnsupportedDefault() {
+ rejects("struct S { int x; }; return \"%(new S)\";", "Cannot format");
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/TerminalInputTest.java b/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
new file mode 100644
index 00000000..bd3831b4
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
@@ -0,0 +1,184 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.util.Ary;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TerminalInputTest {
+ @Test public void testLinkOnlyAndEnterEscape() throws Exception {
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ String output="build/objs/linkOnlyGame/";
+ // Linking must return without displaying the game or waiting for input.
+ assertEquals("",TestC.exec(20,java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"--link","-L",TestC.RELEASE_SYS_DIR,"-o",output,
+ "docs/examples/TicTacToe.smp"));
+ Path exe=Path.of(output,"TicTacToe"+(TestC.OS.startsWith("Windows") ? ".exe" : ""));
+ assertTrue(Files.isRegularFile(exe));
+ Process child=new ProcessBuilder(exe.toString()).redirectErrorStream(true).start();
+ try {
+ child.getOutputStream().write(new byte[]{'\r','\n',27});
+ child.getOutputStream().flush(); // Keep stdin open: Escape must cause the exit.
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ String text=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(text,0,child.exitValue());
+ assertTrue(text,text.endsWith("Your move (1-9): \nGoodbye!\n"));
+ assertFalse(text,text.contains("Choose a square"));
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+
+ @Test public void testRedirectedBytesAndEOF() throws IOException {
+ TestC.run("""
+ val main={ u8[~][] args ->
+ if(sys.io.getch()!='5') return 1;
+ if(sys.io.getch()!='q') return 2;
+ if(sys.io.getch()!=-1) return 3;
+ if(sys.libc.errno()!=sys.libc.EOF) return 4;
+ sys.io.p("ok\n"); return 0;
+ };
+ ""","getchBytes",new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),
+ TestC.CALL_CONVENTION,null,null,"5q","ok\n",-1);
+ }
+
+ @Test public void testReadError() throws IOException {
+ TestC.runSYS("""
+ val main={ u8[~][] args ->
+ sys.io.close(sys.libc.STDIN);
+ if(sys.io.getch()!=-1) return 1;
+ if(sys.libc.errno()<=0) return 2;
+ return 0;
+ };
+ ""","getchError","",-1);
+ }
+
+ @Test public void testGuardsStayInsideFunction() {
+ String src="""
+ val first={ int n -> while(true) { if(n) return 0; return 1; } };
+ val second={ -> int sum=0; for(int i=0; i<3; i++) sum+=i; return sum; };
+ return first(((arg<<32)>>32))+second();
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4",Eval2.eval(code,0));
+ assertEquals("3",Eval2.eval(code,1));
+ }
+ }
+
+ @Test public void testForwardReferenceAfterContinue() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ if(n==1) { n=0; continue; }
+ if(later(n)) return 7;
+ n=0;
+ }
+ return 3;
+ };
+ val later={ int n -> n==2; };
+ return run(((arg<<32)>>32));
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code,1));
+ assertEquals("7",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ // The same merge is used by break, including the implicit loop exit.
+ String breaking=src.replace("return 7;","break;").replace("return 3;","return n+3;");
+ CodeGen code2=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code2,1));
+ assertEquals("5",Eval2.eval(code2,2));
+ assertEquals("3",Eval2.eval(code2,3));
+ }
+ }
+
+ @Test public void testForwardReferenceWithLoopLocal() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ int key=n;
+ n--;
+ if(key==1) continue;
+ if(later(key)) continue;
+ return key;
+ }
+ return 0;
+ };
+ val later={ int key -> key==2; };
+ return run(((arg<<32)>>32));
+ """;
+ String nested=src.replace("if(later(key)) continue;",
+ "if(later(key)) { int local=key+1; continue; }");
+ String breaking=nested.replace("int local=key+1; continue;","int local=key+1; break;")
+ .replace("return 0;","return n+10;");
+ String twoRefs=src.replace("return key;","if(other(key)) continue; return key;")
+ .replace("return run(((arg<<32)>>32));","val other={ int key -> key==3; }; return run(((arg<<32)>>32));");
+ for(long seed:new long[]{1,123,456}) {
+ for(String variant:new String[]{src,nested}) {
+ CodeGen code=new CodeGen(variant,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,1));
+ assertEquals("0",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ }
+ CodeGen code=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,1));
+ assertEquals("11",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ CodeGen code2=new CodeGen(twoRefs,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code2,3));
+ assertEquals("4",Eval2.eval(code2,4));
+ }
+ }
+
+ @Test public void testInteractiveCLIShowsPromptBeforeInput() throws Exception {
+ // Pipe input rather than a terminal: this catches CLI buffering and
+ // requires exactly one byte, with no Enter.
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"-L",TestC.RELEASE_SYS_DIR,"-o","build/objs/interactiveGame/",
+ "docs/examples/TicTacToe.smp").redirectErrorStream(true).start();
+ try(var reader=Executors.newSingleThreadExecutor()) {
+ try {
+ var prompt=reader.submit(() -> readPrompt(child));
+ String opening=prompt.get(20,TimeUnit.SECONDS);
+ assertTrue(opening,opening.contains("You go first."));
+ assertTrue(opening,opening.contains(" | | \n"));
+ assertTrue(child.isAlive());
+ child.getOutputStream().write('5');
+ child.getOutputStream().flush();
+ String turn=reader.submit(() -> readPrompt(child)).get(20,TimeUnit.SECONDS);
+ assertEquals("5\n | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"CPU plays 1.\n O | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"Your move (1-9): ",turn);
+ assertTrue(child.isAlive());
+ child.getOutputStream().write(27);
+ child.getOutputStream().flush();
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ assertEquals("\nGoodbye!\n",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
+ assertEquals(0,child.exitValue());
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+ }
+
+ private static String readPrompt(Process child) throws IOException {
+ var bytes=new ByteArrayOutputStream();
+ while(bytes.size()<8192) {
+ int ch=child.getInputStream().read();
+ if(ch<0) throw new IOException("Exited before prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ bytes.write(ch);
+ String text=bytes.toString(StandardCharsets.UTF_8);
+ if(text.endsWith("Your move (1-9): ")) return text;
+ }
+ throw new IOException("No prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java
index 54adf3af..dd7999c9 100644
--- a/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/src/test/java/com/seaofnodes/simple/TestC.java
@@ -10,10 +10,14 @@ import com.seaofnodes.simple.util.Utils;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutionException;
import static org.junit.Assert.*;
// Call a C driver program, which calls a Simple-generated .o, and self-checks.
@@ -32,10 +36,10 @@ public abstract class TestC {
public static final String CPU_ABI = CPU_PORT+"_"+CALL_CONVENTION;
public static final String RELEASE_SYS_DIR = "build/release/"+CPU_ABI;
- public static String runtimeObject() {
+ public static String runtimeObject(String name) {
String home = System.getenv("SIMPLE_HOME");
String root = home==null || home.isEmpty() ? "build/release" : home;
- return Path.of(root,CPU_ABI,"simple_crt.o").toString();
+ return Path.of(root,CPU_ABI,name+".o").toString();
}
public static final String C_DRIVERS_DIR = "src/test/java/com/seaofnodes/simple/progs/";
@@ -176,7 +180,7 @@ public abstract class TestC {
linkExe(objects,c_conv,cfile,exe);
}
- private static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException {
+ static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException {
// Compile the C program. Compiling code and constants in the low
// 2Gig. Pointers are 64b BUT since always in the low 2G all the
// high bits are zero - and Simple code can be emitted treating
@@ -187,9 +191,12 @@ public abstract class TestC {
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
// Standalone Simple programs export `simple_main`; the small C
- // runtime owns the native main symbol and converts argc/argv into a
- // normal Simple array before making the ABI-qualified call.
- if( cfile==null ) params.add(runtimeObject());
+ // runtime owns native main and calls the Simple argument converter
+ // before making the ABI-qualified entry call.
+ if( cfile==null ) {
+ params.add(runtimeObject("simple_crt"));
+ params.add(runtimeObject("simple_args"));
+ }
params.addAll(new String[] {
"-lm", // Picks up 'sqrt' for newtonFloat tests to compare
"-g", // debug symbols
@@ -215,6 +222,40 @@ public abstract class TestC {
return execStdin(null,seconds,args);
}
+ // The CLI must display prompts while the child is waiting for input.
+ // Keep the capture-to-files path below for bounded, noninteractive tests.
+ public static void execInteractive(String... args) throws IOException {
+ Process child = new ProcessBuilder(args).redirectInput(ProcessBuilder.Redirect.INHERIT).start();
+ try( var relays = Executors.newFixedThreadPool(2) ) {
+ var out = relays.submit(() -> { relay(child.getInputStream(),System.out); return null; });
+ var err = relays.submit(() -> { relay(child.getErrorStream(),System.err); return null; });
+ try {
+ int status = child.waitFor();
+ out.get();
+ err.get();
+ if( status!=0 ) throw new IOException(String.join(" ",args)+"\nexec exit code: "+status);
+ } catch( InterruptedException e ) {
+ child.destroyForcibly();
+ Thread.currentThread().interrupt();
+ throw new IOException("interrupted",e);
+ } catch( ExecutionException e ) {
+ throw new IOException("Cannot relay program output",e.getCause());
+ } finally {
+ if( child.isAlive() ) child.destroyForcibly();
+ }
+ }
+ }
+
+ private static void relay(InputStream in, PrintStream out) throws IOException {
+ try( in ) {
+ byte[] buffer = new byte[4096];
+ for( int n; (n=in.read(buffer))!=-1; ) {
+ out.write(buffer,0,n);
+ out.flush();
+ }
+ }
+ }
+
private static String execStdin( String stdin, int seconds, String... args ) throws IOException {
ExecResult result = execute(stdin,seconds,args);
if( result.exit!=0 )
diff --git a/src/test/java/com/seaofnodes/simple/TicTacToeTest.java b/src/test/java/com/seaofnodes/simple/TicTacToeTest.java
new file mode 100644
index 00000000..1d267935
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TicTacToeTest.java
@@ -0,0 +1,92 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TicTacToeTest {
+ private static String exe;
+
+ @BeforeClass public static void compileGame() throws Exception {
+ exe=TestC.compile(Files.readString(Path.of("docs/examples/TicTacToe.smp")),
+ "ticTacToeGames",new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),
+ TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String play(String keys) throws Exception {
+ Process child=new ProcessBuilder(exe).redirectErrorStream(true).start();
+ try {
+ child.getOutputStream().write(keys.getBytes(StandardCharsets.US_ASCII));
+ child.getOutputStream().close();
+ assertTrue("Game did not finish",child.waitFor(10,TimeUnit.SECONDS));
+ String text=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(text,0,child.exitValue());
+ return text;
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+
+ private static String board(String squares) {
+ StringBuilder out=new StringBuilder();
+ for(int row=0;row<3;row++) {
+ if(row>0) out.append("---+---+---\n");
+ for(int col=0;col<3;col++) {
+ if(col>0) out.append('|');
+ out.append(' ').append(squares.charAt(row*3+col)).append(' ');
+ }
+ out.append('\n');
+ }
+ return out.toString();
+ }
+
+ @Test public void testPlayerWin() throws Exception {
+ String text=play("1879");
+ assertTrue(text,text.endsWith(board("X OOO XXX")+"You win!\n"));
+ assertEquals(3,text.split("CPU plays ",-1).length-1);
+ }
+
+ @Test public void testCPUWinsEveryLine() throws Exception {
+ // Three rows, three columns, and both diagonals in complete games.
+ String[][] games={
+ {"4697","OOOXOXX X"}, {"1278","XXOOOOXX "}, {"5234","OXXXX OOO"},
+ {"2893","OXXOO OXX"}, {"134","XOXXO O "}, {"5472","OXOXXOX O"},
+ {"234","OXXXO O"}, {"124","XXOXO O "}
+ };
+ for(String[] game:games) {
+ String text=play(game[0]);
+ assertTrue(game[0]+"\n"+text,text.endsWith(board(game[1])+"CPU wins!\n"));
+ }
+ }
+
+ @Test public void testDraw() throws Exception {
+ String text=play("12768");
+ assertTrue(text,text.endsWith(board("XXOOOXXXO")+"It's a draw!\n"));
+ assertEquals(4,text.split("CPU plays ",-1).length-1);
+ }
+
+ @Test public void testBusySquaresAndInvalidInputDoNotTakeATurn() throws Exception {
+ // X takes 5 and O takes 1; neither occupied square can be overwritten.
+ String text=play("x\r\n551q");
+ assertEquals(text,1,text.split("Choose a square",-1).length-1);
+ assertEquals(text,2,text.split("That square is occupied",-1).length-1);
+ assertEquals(text,1,text.split("CPU plays ",-1).length-1);
+ assertTrue(text,text.contains("Your move (1-9): \nChoose a square from 1 to 9: 5\n"
+ +board(" X ")));
+ assertTrue(text,text.contains(board("O X ")));
+ assertTrue(text,text.endsWith("Your move (1-9): "
+ +"\nThat square is occupied. Your move (1-9): "
+ +"\nThat square is occupied. Your move (1-9): \nGoodbye!\n"));
+ }
+
+ @Test public void testCPUBlocksAndEOFQuits() throws Exception {
+ String text=play("12");
+ assertTrue(text,text.contains("CPU plays 3.\n"+board("XXO O ")));
+ assertTrue(text,text.endsWith("Goodbye!\n"));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
index adbb10a5..edb2519c 100644
--- a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
+++ b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
@@ -91,12 +91,52 @@ public class RegAllocTestSupport {
public static void nullUseMask() {
CodeGen code = graph();
Op def = new Op(A,null,null,false,code._start);
+ new Op(null,null,A,false,code._start); // The dependency must not keep A live across this kill.
new Op(null,null,null,false,code._start,def); // Scheduling dependency only.
RegAlloc alloc = new RegAlloc(code);
assertTrue(BuildLRG.run(0,alloc));
assertTrue(IFG.build(0,alloc));
}
+ public static void independentFunctions() {
+ for( boolean reverse : new boolean[]{false,true} ) {
+ CodeGen code = graph();
+ code._cfg.clear();
+ code._callerSave = RegMask.FULL; // These graphs only test local constraints.
+ FunNode masks = new FunNode(null,com.seaofnodes.simple.type.TypeFunPtr.BOT,
+ "masks",null,null,code._start);
+ Op a = new Op(A,null,null,false,masks);
+ Op b = new Op(null,B,null,false,masks,a); // Fails BuildLRG.
+ ReturnNode ret1 = new ReturnNode(masks,null,null,null,masks);
+ masks.setRet(ret1);
+
+ FunNode kills = new FunNode(null,com.seaofnodes.simple.type.TypeFunPtr.BOT,
+ "kills",null,null,code._start);
+ Op c = new Op(A,null,null,false,kills);
+ new Op(null,null,A,false,kills);
+ Op d = new Op(null,A,null,false,kills,c); // Fails IFG, independently of masks.
+ ReturnNode ret2 = new ReturnNode(kills,null,null,null,kills);
+ kills.setRet(ret2);
+ code._cfg.add(reverse ? kills : masks);
+ code._cfg.add(reverse ? ret2 : ret1);
+ code._cfg.add(reverse ? masks : kills);
+ code._cfg.add(reverse ? ret1 : ret2);
+ RegAlloc alloc = new RegAlloc(code);
+ alloc.regAlloc();
+ assertEquals(0,alloc.regnum(a));
+ assertEquals(1,alloc.regnum(b.in(1)));
+ assertEquals(0,alloc.regnum(c));
+ assertEquals(0,alloc.regnum(d.in(1)));
+ int splits = 0;
+ for( Node n : kills.outs() )
+ if( n instanceof SplitNode split ) {
+ assertEquals("Another function's mask failure must not delay this split",0,split._round);
+ splits++;
+ }
+ assertTrue("Preserving A across the kill requires transfers",splits>0);
+ }
+ }
+
public static void cloneRegisterClass() {
for( RegMask required : new RegMask[]{B,new RegMask(6L)} ) {
CodeGen code = graph();
@@ -144,7 +184,7 @@ public class RegAllocTestSupport {
assertEquals(op,1,found);
code.driver(CodeGen.Phase.RegAlloc,"x86_64_v2","SystemV");
checkRegisters(code);
- assertEquals(op+" reversed="+reversed,8,code._regAlloc._spillScaled);
+ assertEquals(op+" reversed="+reversed,24,code._regAlloc._spillScaled);
code.encode();
}
}
@@ -307,6 +347,34 @@ public class RegAllocTestSupport {
}
}
+ public static void tiedLoopBackedges() {
+ CodeGen code = graph();
+ RegAlloc alloc = new RegAlloc(code);
+ Op init = new Op(A,null,null,false,code._start);
+ LoopNode loop = new LoopNode(null,null,code._start);
+ PhiNode first = new PhiNode("first",loop,init,null);
+ PhiNode second = new PhiNode("second",loop,init,null);
+ first._type = second._type = TypeInteger.BOT;
+ Op back = new Op(A,A,null,false,loop,first);
+ Op narrowed = new Op(A,A,null,false,loop,back);
+ first.setDef(2,back);
+ second.setDef(2,narrowed);
+ new Op(null,A,null,false,loop,first,second);
+ loop.setDef(2,loop);
+ // Model the union created by two-address operations on the backedge.
+ LRG lrg = alloc.newLRG(first,null);
+ lrg._mask = A;
+ for( Node n : new Node[]{second,init,back,narrowed} ) alloc.union(lrg,n);
+ lrg.selfConflict(first);
+ lrg.selfConflict(second);
+ alloc.splitSelfConflict((byte)0,lrg);
+ assertSame(back,first.in(2));
+ assertSame(narrowed,second.in(2));
+ alloc.splitSelfConflict((byte)1,lrg);
+ assertTrue(first.in(2) instanceof SplitNode);
+ assertTrue(second.in(2) instanceof SplitNode);
+ }
+
public static void copyForward() throws Exception {
// Forwarding, fixed operand, two-address tie, intervening clobber,
// another user, and a scheduling-only edge.
diff --git a/src/test/java/com/seaofnodes/simple/progs/extern_data.c b/src/test/java/com/seaofnodes/simple/progs/extern_data.c
index 9325f65e..e8156d0f 100644
--- a/src/test/java/com/seaofnodes/simple/progs/extern_data.c
+++ b/src/test/java/com/seaofnodes/simple/progs/extern_data.c
@@ -7,8 +7,13 @@ double fraction=1.25;
int64_t bump(void) { counter+=5; return counter; }
__attribute__((CALL_CONV)) extern int64_t test_global(void);
+__attribute__((CALL_CONV)) extern int64_t extern_matches(int32_t);
+__attribute__((CALL_CONV)) extern int64_t extern_less(int32_t);
+__attribute__((CALL_CONV)) extern int64_t extern_seven(void);
int main(void) {
+ if (!extern_matches(7) || extern_matches(8) || !extern_seven() ||
+ !extern_less(6) || extern_less(7) || extern_less(8)) return 2;
int64_t result=test_global();
if(result!=71515 || counter!=15 || small!=-2 || fraction!=1.75) {
printf("result=%lld counter=%d small=%d fraction=%g\n",
@@ -17,6 +22,8 @@ int main(void) {
}
// A second call must observe C's new values, not a Simple initialization copy.
counter=-2;
+ if (!extern_matches(-2) || extern_matches(7) || extern_seven() ||
+ !extern_less(-3) || extern_less(-2)) return 3;
result=test_global();
return result!=-19394 || counter!=6 || small!=-1 || fraction!=2.25;
}
diff --git a/src/test/java/com/seaofnodes/simple/progs/int32.c b/src/test/java/com/seaofnodes/simple/progs/int32.c
new file mode 100644
index 00000000..9694c47b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/progs/int32.c
@@ -0,0 +1,26 @@
+#include <stdint.h>
+#include <stdio.h>
+
+typedef struct { int32_t x, y; } Pair;
+typedef struct { uint32_t len; int32_t values[]; } Ints;
+
+__attribute__((CALL_CONV)) extern int64_t int_echo(int32_t);
+__attribute__((CALL_CONV)) extern int64_t int_wrap(int32_t);
+__attribute__((CALL_CONV)) extern int64_t wide_echo(int64_t);
+__attribute__((CALL_CONV)) extern int64_t narrow_sum(int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,int32_t,uint32_t);
+__attribute__((CALL_CONV)) extern Pair *int_pair(void);
+__attribute__((CALL_CONV)) extern Ints *int_array(void);
+
+int main(void) {
+ if (int_echo(-1) != -1 || int_echo(INT32_MIN) != INT32_MIN) return 1;
+ if (int_wrap(INT32_MAX) != INT32_MIN) return 2;
+ if (wide_echo(INT64_MAX) != INT64_MAX || wide_echo(INT64_MIN) != INT64_MIN) return 3;
+ int64_t expected = -128LL + 255 - 32768 + 65535 + INT32_MIN + UINT32_MAX - 7 + UINT32_MAX;
+ if (narrow_sum(-128,255,-32768,65535,INT32_MIN,UINT32_MAX,-7,UINT32_MAX) != expected) return 4;
+ Pair *p = int_pair();
+ if (p->x != -1 || p->y != INT32_MAX) return 5;
+ Ints *a = int_array();
+ if (a->len != 3 || a->values[0] != -1 || a->values[1] != INT32_MAX || a->values[2] != INT32_MIN) return 6;
+ puts("ok");
+ return 0;
+}
diff --git a/src/test/java/com/seaofnodes/simple/progs/runtime_args.c b/src/test/java/com/seaofnodes/simple/progs/runtime_args.c
new file mode 100644
index 00000000..cd676006
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/progs/runtime_args.c
@@ -0,0 +1,35 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+typedef struct { uint32_t len; unsigned char bytes[]; } Bytes;
+typedef struct { uint32_t len, pad; Bytes *args[]; } Args;
+
+__attribute__((CALL_CONV))
+extern Args *simple_args(int64_t argc, char **argv);
+
+const char *simple_argv(char **argv, int64_t index) { return argv[index]; }
+
+int main(void) {
+ Args *empty = simple_args(0, NULL);
+ if (!empty || empty->len != 0) return 1;
+ if (simple_args(-1, NULL) != NULL) return 2;
+ if (simple_args(4294967296LL, NULL) != NULL) return 6;
+
+ char name[] = "program", blank[] = "", utf8[] = "\xc3\xa9\xcf\x80";
+ char *argv[] = { name, blank, utf8, name };
+ Args *args = simple_args(4, argv);
+ if (!args || args->len != 4) return 3;
+ for (int i = 0; i < 4; i++) {
+ if (!args->args[i] || args->args[i]->len != strlen(argv[i]) ||
+ memcmp(args->args[i]->bytes, argv[i], args->args[i]->len)) return 4;
+ }
+ // Copies survive changes to the native buffers, and repeated argv pointers
+ // still produce independent Simple arrays.
+ name[0] = 'X';
+ utf8[0] = 0;
+ args->args[0]->bytes[0] = 'Y';
+ if (args->args[3]->bytes[0] != 'p' || args->args[2]->bytes[0] != 0xc3) return 5;
+ puts("ok");
+ return 0;
+}
diff --git a/src/test/java/com/seaofnodes/simple/progs/sort.c b/src/test/java/com/seaofnodes/simple/progs/sort.c
index ad76cc7f..08acf2da 100644
--- a/src/test/java/com/seaofnodes/simple/progs/sort.c
+++ b/src/test/java/com/seaofnodes/simple/progs/sort.c
@@ -5,14 +5,14 @@
typedef struct AryInt {
unsigned int len;
- long long data[];
+ int data[];
} AryInt;
__attribute__ ((CALL_CONV))
extern AryInt* merge_sort(AryInt*);
-long long primes[] = { 25, 97, 89, 83, 79, 73, 71, 67, 61, 59, 53, 47, 43, 41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
+int primes[] = { 25, 97, 89, 83, 79, 73, 71, 67, 61, 59, 53, 47, 43, 41, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
int main() {
AryInt *as = (AryInt*)primes;
diff --git a/src/test/java/com/seaofnodes/simple/progs/stream_io.c b/src/test/java/com/seaofnodes/simple/progs/stream_io.c
new file mode 100644
index 00000000..5cd1096a
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/progs/stream_io.c
@@ -0,0 +1,49 @@
+#include <errno.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <unistd.h>
+
+// Deterministic short I/O and failures; ordinary descriptors still use libc.
+extern ssize_t __real_read(int, void *, size_t);
+extern ssize_t __real_write(int, const void *, size_t);
+static int mode, calls, bad;
+static int64_t count, offset;
+
+int32_t test_io_mode(int32_t next, int64_t start) {
+ mode = next;
+ calls = bad = 0;
+ count = 0;
+ offset = start;
+ return 0;
+}
+int64_t test_io_count(void) { return count; }
+int32_t test_io_status(void) {
+ return bad || (mode == 5 && (errno != EAGAIN || calls != 1));
+}
+
+ssize_t __wrap_read(int fd, void *buffer, size_t size) {
+ if (fd != 700) return __real_read(fd, buffer, size);
+ ++calls;
+ if (mode == 1 && calls == 1) { errno = EINTR; return -1; }
+ if (mode == 2 && calls > 1) { errno = EIO; return -1; }
+ if (count == 24593) return 0;
+ size_t n = size < 137 ? size : 137;
+ if (n > (size_t)(24593-count)) n = (size_t)(24593-count);
+ for (size_t i=0; i<n; ++i) ((unsigned char *)buffer)[i] = (count+i)%251;
+ count += n;
+ return (ssize_t)n;
+}
+
+ssize_t __wrap_write(int fd, const void *buffer, size_t size) {
+ if (fd != 701) return __real_write(fd, buffer, size);
+ ++calls;
+ if (mode == 1 && calls == 1) { errno = EINTR; return -1; }
+ if (mode == 3 && calls > 1) { errno = EIO; return -1; }
+ if (mode == 4) return 0;
+ if (mode == 5) { errno = EAGAIN; return -1; }
+ size_t n = size < 7 ? size : 7;
+ for (size_t i=0; i<n; ++i)
+ if (((const unsigned char *)buffer)[i] != (offset+count+i)%251) bad = 1;
+ count += n;
+ return (ssize_t)n;
+}
diff --git a/src/test/java/com/seaofnodes/simple/progs/stringHash.c b/src/test/java/com/seaofnodes/simple/progs/stringHash.c
index 2c6d0fa9..6578c852 100644
--- a/src/test/java/com/seaofnodes/simple/progs/stringHash.c
+++ b/src/test/java/com/seaofnodes/simple/progs/stringHash.c
@@ -7,7 +7,7 @@ typedef struct ArrayU8 {
} ArrayU8;
typedef struct String {
ArrayU8 * cs;
- long long _hashCode;
+ int _hashCode;
} String;
diff --git a/src/test/smp/separate/client/Main.smp b/src/test/smp/separate/client/Main.smp
index 077f6575..e70079db 100644
--- a/src/test/smp/separate/client/Main.smp
+++ b/src/test/smp/separate/client/Main.smp
@@ -3,4 +3,4 @@ val run={int n ->
val c=new Counters.Counter(n);
return c.get()+100*Counters.next_noInline();
};
-return run(arg);
+int n=arg; return run(n);